3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2018 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2018-03-07T06:52:35Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
65 * Generate a unique ID for element
69 OO
.ui
.generateElementId = function () {
71 return 'ooui-' + OO
.ui
.elementId
;
75 * Check if an element is focusable.
76 * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
79 * @return {boolean} Element is focusable
81 OO
.ui
.isFocusableElement = function ( $element
) {
83 element
= $element
[ 0 ];
85 // Anything disabled is not focusable
86 if ( element
.disabled
) {
90 // Check if the element is visible
92 // This is quicker than calling $element.is( ':visible' )
93 $.expr
.pseudos
.visible( element
) &&
94 // Check that all parents are visible
95 !$element
.parents().addBack().filter( function () {
96 return $.css( this, 'visibility' ) === 'hidden';
102 // Check if the element is ContentEditable, which is the string 'true'
103 if ( element
.contentEditable
=== 'true' ) {
107 // Anything with a non-negative numeric tabIndex is focusable.
108 // Use .prop to avoid browser bugs
109 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
113 // Some element types are naturally focusable
114 // (indexOf is much faster than regex in Chrome and about the
115 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116 nodeName
= element
.nodeName
.toLowerCase();
117 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
121 // Links and areas are focusable if they have an href
122 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
130 * Find a focusable child
132 * @param {jQuery} $container Container to search in
133 * @param {boolean} [backwards] Search backwards
134 * @return {jQuery} Focusable child, or an empty jQuery object if none found
136 OO
.ui
.findFocusable = function ( $container
, backwards
) {
137 var $focusable
= $( [] ),
138 // $focusableCandidates is a superset of things that
139 // could get matched by isFocusableElement
140 $focusableCandidates
= $container
141 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
144 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
147 $focusableCandidates
.each( function () {
148 var $this = $( this );
149 if ( OO
.ui
.isFocusableElement( $this ) ) {
158 * Get the user's language and any fallback languages.
160 * These language codes are used to localize user interface elements in the user's language.
162 * In environments that provide a localization system, this function should be overridden to
163 * return the user's language(s). The default implementation returns English (en) only.
165 * @return {string[]} Language codes, in descending order of priority
167 OO
.ui
.getUserLanguages = function () {
172 * Get a value in an object keyed by language code.
174 * @param {Object.<string,Mixed>} obj Object keyed by language code
175 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176 * @param {string} [fallback] Fallback code, used if no matching language can be found
177 * @return {Mixed} Local value
179 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
182 // Requested language
186 // Known user language
187 langs
= OO
.ui
.getUserLanguages();
188 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
195 if ( obj
[ fallback
] ) {
196 return obj
[ fallback
];
198 // First existing language
199 for ( lang
in obj
) {
207 * Check if a node is contained within another node
209 * Similar to jQuery#contains except a list of containers can be supplied
210 * and a boolean argument allows you to include the container in the match list
212 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213 * @param {HTMLElement} contained Node to find
214 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
215 * @return {boolean} The node is in the list of target nodes
217 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
219 if ( !Array
.isArray( containers
) ) {
220 containers
= [ containers
];
222 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
223 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
231 * Return a function, that, as long as it continues to be invoked, will not
232 * be triggered. The function will be called after it stops being called for
233 * N milliseconds. If `immediate` is passed, trigger the function on the
234 * leading edge, instead of the trailing.
236 * Ported from: http://underscorejs.org/underscore.js
238 * @param {Function} func Function to debounce
239 * @param {number} [wait=0] Wait period in milliseconds
240 * @param {boolean} [immediate] Trigger on leading edge
241 * @return {Function} Debounced function
243 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
248 later = function () {
251 func
.apply( context
, args
);
254 if ( immediate
&& !timeout
) {
255 func
.apply( context
, args
);
257 if ( !timeout
|| wait
) {
258 clearTimeout( timeout
);
259 timeout
= setTimeout( later
, wait
);
265 * Puts a console warning with provided message.
267 * @param {string} message Message
269 OO
.ui
.warnDeprecation = function ( message
) {
270 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
271 // eslint-disable-next-line no-console
272 console
.warn( message
);
277 * Returns a function, that, when invoked, will only be triggered at most once
278 * during a given window of time. If called again during that window, it will
279 * wait until the window ends and then trigger itself again.
281 * As it's not knowable to the caller whether the function will actually run
282 * when the wrapper is called, return values from the function are entirely
285 * @param {Function} func Function to throttle
286 * @param {number} wait Throttle window length, in milliseconds
287 * @return {Function} Throttled function
289 OO
.ui
.throttle = function ( func
, wait
) {
290 var context
, args
, timeout
,
294 previous
= OO
.ui
.now();
295 func
.apply( context
, args
);
298 // Check how long it's been since the last time the function was
299 // called, and whether it's more or less than the requested throttle
300 // period. If it's less, run the function immediately. If it's more,
301 // set a timeout for the remaining time -- but don't replace an
302 // existing timeout, since that'd indefinitely prolong the wait.
303 var remaining
= wait
- ( OO
.ui
.now() - previous
);
306 if ( remaining
<= 0 ) {
307 // Note: unless wait was ridiculously large, this means we'll
308 // automatically run the first time the function was called in a
309 // given period. (If you provide a wait period larger than the
310 // current Unix timestamp, you *deserve* unexpected behavior.)
311 clearTimeout( timeout
);
313 } else if ( !timeout
) {
314 timeout
= setTimeout( run
, remaining
);
320 * A (possibly faster) way to get the current timestamp as an integer
322 * @return {number} Current timestamp, in milliseconds since the Unix epoch
324 OO
.ui
.now
= Date
.now
|| function () {
325 return new Date().getTime();
329 * Reconstitute a JavaScript object corresponding to a widget created by
330 * the PHP implementation.
332 * This is an alias for `OO.ui.Element.static.infuse()`.
334 * @param {string|HTMLElement|jQuery} idOrNode
335 * A DOM id (if a string) or node for the widget to infuse.
336 * @return {OO.ui.Element}
337 * The `OO.ui.Element` corresponding to this (infusable) document node.
339 OO
.ui
.infuse = function ( idOrNode
) {
340 return OO
.ui
.Element
.static.infuse( idOrNode
);
345 * Message store for the default implementation of OO.ui.msg
347 * Environments that provide a localization system should not use this, but should override
348 * OO.ui.msg altogether.
353 // Tool tip for a button that moves items in a list down one place
354 'ooui-outline-control-move-down': 'Move item down',
355 // Tool tip for a button that moves items in a list up one place
356 'ooui-outline-control-move-up': 'Move item up',
357 // Tool tip for a button that removes items from a list
358 'ooui-outline-control-remove': 'Remove item',
359 // Label for the toolbar group that contains a list of all other available tools
360 'ooui-toolbar-more': 'More',
361 // Label for the fake tool that expands the full list of tools in a toolbar group
362 'ooui-toolgroup-expand': 'More',
363 // Label for the fake tool that collapses the full list of tools in a toolbar group
364 'ooui-toolgroup-collapse': 'Fewer',
365 // Default label for the tooltip for the button that removes a tag item
366 'ooui-item-remove': 'Remove',
367 // Default label for the accept button of a confirmation dialog
368 'ooui-dialog-message-accept': 'OK',
369 // Default label for the reject button of a confirmation dialog
370 'ooui-dialog-message-reject': 'Cancel',
371 // Title for process dialog error description
372 'ooui-dialog-process-error': 'Something went wrong',
373 // Label for process dialog dismiss error button, visible when describing errors
374 'ooui-dialog-process-dismiss': 'Dismiss',
375 // Label for process dialog retry action button, visible when describing only recoverable errors
376 'ooui-dialog-process-retry': 'Try again',
377 // Label for process dialog retry action button, visible when describing only warnings
378 'ooui-dialog-process-continue': 'Continue',
379 // Label for the file selection widget's select file button
380 'ooui-selectfile-button-select': 'Select a file',
381 // Label for the file selection widget if file selection is not supported
382 'ooui-selectfile-not-supported': 'File selection is not supported',
383 // Label for the file selection widget when no file is currently selected
384 'ooui-selectfile-placeholder': 'No file is selected',
385 // Label for the file selection widget's drop target
386 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
390 * Get a localized message.
392 * After the message key, message parameters may optionally be passed. In the default implementation,
393 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
394 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
395 * they support unnamed, ordered message parameters.
397 * In environments that provide a localization system, this function should be overridden to
398 * return the message translated in the user's language. The default implementation always returns
399 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
403 * var i, iLen, button,
404 * messagePath = 'oojs-ui/dist/i18n/',
405 * languages = [ $.i18n().locale, 'ur', 'en' ],
408 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
409 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
412 * $.i18n().load( languageMap ).done( function() {
413 * // Replace the built-in `msg` only once we've loaded the internationalization.
414 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
415 * // you put off creating any widgets until this promise is complete, no English
416 * // will be displayed.
417 * OO.ui.msg = $.i18n;
419 * // A button displaying "OK" in the default locale
420 * button = new OO.ui.ButtonWidget( {
421 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
424 * $( 'body' ).append( button.$element );
426 * // A button displaying "OK" in Urdu
427 * $.i18n().locale = 'ur';
428 * button = new OO.ui.ButtonWidget( {
429 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
432 * $( 'body' ).append( button.$element );
435 * @param {string} key Message key
436 * @param {...Mixed} [params] Message parameters
437 * @return {string} Translated message with parameters substituted
439 OO
.ui
.msg = function ( key
) {
440 var message
= messages
[ key
],
441 params
= Array
.prototype.slice
.call( arguments
, 1 );
442 if ( typeof message
=== 'string' ) {
443 // Perform $1 substitution
444 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
445 var i
= parseInt( n
, 10 );
446 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
449 // Return placeholder if message not found
450 message
= '[' + key
+ ']';
457 * Package a message and arguments for deferred resolution.
459 * Use this when you are statically specifying a message and the message may not yet be present.
461 * @param {string} key Message key
462 * @param {...Mixed} [params] Message parameters
463 * @return {Function} Function that returns the resolved message when executed
465 OO
.ui
.deferMsg = function () {
466 var args
= arguments
;
468 return OO
.ui
.msg
.apply( OO
.ui
, args
);
475 * If the message is a function it will be executed, otherwise it will pass through directly.
477 * @param {Function|string} msg Deferred message, or message text
478 * @return {string} Resolved message
480 OO
.ui
.resolveMsg = function ( msg
) {
481 if ( $.isFunction( msg
) ) {
488 * @param {string} url
491 OO
.ui
.isSafeUrl = function ( url
) {
492 // Keep this function in sync with php/Tag.php
493 var i
, protocolWhitelist
;
495 function stringStartsWith( haystack
, needle
) {
496 return haystack
.substr( 0, needle
.length
) === needle
;
499 protocolWhitelist
= [
500 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
501 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
502 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
509 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
510 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
515 // This matches '//' too
516 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
519 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
527 * Check if the user has a 'mobile' device.
529 * For our purposes this means the user is primarily using an
530 * on-screen keyboard, touch input instead of a mouse and may
531 * have a physically small display.
533 * It is left up to implementors to decide how to compute this
534 * so the default implementation always returns false.
536 * @return {boolean} Use is on a mobile device
538 OO
.ui
.isMobile = function () {
543 * Get the additional spacing that should be taken into account when displaying elements that are
544 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
545 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
547 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
548 * the extra spacing from that edge of viewport (in pixels)
550 OO
.ui
.getViewportSpacing = function () {
560 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
561 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
563 * @return {jQuery} Default overlay node
565 OO
.ui
.getDefaultOverlay = function () {
566 if ( !OO
.ui
.$defaultOverlay
) {
567 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
568 $( 'body' ).append( OO
.ui
.$defaultOverlay
);
570 return OO
.ui
.$defaultOverlay
;
578 * Namespace for OOUI mixins.
580 * Mixins are named according to the type of object they are intended to
581 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
582 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
583 * is intended to be mixed in to an instance of OO.ui.Widget.
591 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
592 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
593 * connected to them and can't be interacted with.
599 * @param {Object} [config] Configuration options
600 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
601 * to the top level (e.g., the outermost div) of the element. See the [OOUI documentation on MediaWiki][2]
603 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
604 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
605 * @cfg {string} [text] Text to insert
606 * @cfg {Array} [content] An array of content elements to append (after #text).
607 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
608 * Instances of OO.ui.Element will have their $element appended.
609 * @cfg {jQuery} [$content] Content elements to append (after #text).
610 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
611 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
612 * Data can also be specified with the #setData method.
614 OO
.ui
.Element
= function OoUiElement( config
) {
615 if ( OO
.ui
.isDemo
) {
616 this.initialConfig
= config
;
618 // Configuration initialization
619 config
= config
|| {};
623 this.elementId
= null;
625 this.data
= config
.data
;
626 this.$element
= config
.$element
||
627 $( document
.createElement( this.getTagName() ) );
628 this.elementGroup
= null;
631 if ( Array
.isArray( config
.classes
) ) {
632 this.$element
.addClass( config
.classes
.join( ' ' ) );
635 this.setElementId( config
.id
);
638 this.$element
.text( config
.text
);
640 if ( config
.content
) {
641 // The `content` property treats plain strings as text; use an
642 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
643 // appropriate $element appended.
644 this.$element
.append( config
.content
.map( function ( v
) {
645 if ( typeof v
=== 'string' ) {
646 // Escape string so it is properly represented in HTML.
647 return document
.createTextNode( v
);
648 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
651 } else if ( v
instanceof OO
.ui
.Element
) {
657 if ( config
.$content
) {
658 // The `$content` property treats plain strings as HTML.
659 this.$element
.append( config
.$content
);
665 OO
.initClass( OO
.ui
.Element
);
667 /* Static Properties */
670 * The name of the HTML tag used by the element.
672 * The static value may be ignored if the #getTagName method is overridden.
678 OO
.ui
.Element
.static.tagName
= 'div';
683 * Reconstitute a JavaScript object corresponding to a widget created
684 * by the PHP implementation.
686 * @param {string|HTMLElement|jQuery} idOrNode
687 * A DOM id (if a string) or node for the widget to infuse.
688 * @return {OO.ui.Element}
689 * The `OO.ui.Element` corresponding to this (infusable) document node.
690 * For `Tag` objects emitted on the HTML side (used occasionally for content)
691 * the value returned is a newly-created Element wrapping around the existing
694 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
695 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
696 // Verify that the type matches up.
697 // FIXME: uncomment after T89721 is fixed, see T90929.
699 if ( !( obj instanceof this['class'] ) ) {
700 throw new Error( 'Infusion type mismatch!' );
707 * Implementation helper for `infuse`; skips the type check and has an
708 * extra property so that only the top-level invocation touches the DOM.
711 * @param {string|HTMLElement|jQuery} idOrNode
712 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
713 * when the top-level widget of this infusion is inserted into DOM,
714 * replacing the original node; or false for top-level invocation.
715 * @return {OO.ui.Element}
717 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
718 // look for a cached result of a previous infusion.
719 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
720 if ( typeof idOrNode
=== 'string' ) {
722 $elem
= $( document
.getElementById( id
) );
724 $elem
= $( idOrNode
);
725 id
= $elem
.attr( 'id' );
727 if ( !$elem
.length
) {
728 if ( typeof idOrNode
=== 'string' ) {
729 error
= 'Widget not found: ' + idOrNode
;
730 } else if ( idOrNode
&& idOrNode
.selector
) {
731 error
= 'Widget not found: ' + idOrNode
.selector
;
733 error
= 'Widget not found';
735 throw new Error( error
);
737 if ( $elem
[ 0 ].oouiInfused
) {
738 $elem
= $elem
[ 0 ].oouiInfused
;
740 data
= $elem
.data( 'ooui-infused' );
743 if ( data
=== true ) {
744 throw new Error( 'Circular dependency! ' + id
);
747 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
748 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
749 // restore dynamic state after the new element is re-inserted into DOM under infused parent
750 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
751 infusedChildren
= $elem
.data( 'ooui-infused-children' );
752 if ( infusedChildren
&& infusedChildren
.length
) {
753 infusedChildren
.forEach( function ( data
) {
754 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
755 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
761 data
= $elem
.attr( 'data-ooui' );
763 throw new Error( 'No infusion data found: ' + id
);
766 data
= JSON
.parse( data
);
770 if ( !( data
&& data
._
) ) {
771 throw new Error( 'No valid infusion data found: ' + id
);
773 if ( data
._
=== 'Tag' ) {
774 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
775 return new OO
.ui
.Element( { $element
: $elem
} );
777 parts
= data
._
.split( '.' );
778 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
779 if ( cls
=== undefined ) {
780 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
783 // Verify that we're creating an OO.ui.Element instance
786 while ( parent
!== undefined ) {
787 if ( parent
=== OO
.ui
.Element
) {
792 parent
= parent
.parent
;
795 if ( parent
!== OO
.ui
.Element
) {
796 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
799 if ( domPromise
=== false ) {
801 domPromise
= top
.promise();
803 $elem
.data( 'ooui-infused', true ); // prevent loops
804 data
.id
= id
; // implicit
805 infusedChildren
= [];
806 data
= OO
.copy( data
, null, function deserialize( value
) {
808 if ( OO
.isPlainObject( value
) ) {
810 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
811 infusedChildren
.push( infused
);
812 // Flatten the structure
813 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
814 infused
.$element
.removeData( 'ooui-infused-children' );
817 if ( value
.html
!== undefined ) {
818 return new OO
.ui
.HtmlSnippet( value
.html
);
822 // allow widgets to reuse parts of the DOM
823 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
824 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
825 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
827 // eslint-disable-next-line new-cap
828 obj
= new cls( data
);
829 // If anyone is holding a reference to the old DOM element,
830 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
831 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
832 $elem
[ 0 ].oouiInfused
= obj
.$element
;
833 // now replace old DOM with this new DOM.
835 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
836 // so only mutate the DOM if we need to.
837 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
838 $elem
.replaceWith( obj
.$element
);
842 obj
.$element
.data( 'ooui-infused', obj
);
843 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
844 // set the 'data-ooui' attribute so we can identify infused widgets
845 obj
.$element
.attr( 'data-ooui', '' );
846 // restore dynamic state after the new element is inserted into DOM
847 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
852 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
854 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
855 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
856 * constructor, which will be given the enhanced config.
859 * @param {HTMLElement} node
860 * @param {Object} config
863 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
868 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
869 * (and its children) that represent an Element of the same class and the given configuration,
870 * generated by the PHP implementation.
872 * This method is called just before `node` is detached from the DOM. The return value of this
873 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
874 * is inserted into DOM to replace `node`.
877 * @param {HTMLElement} node
878 * @param {Object} config
881 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
886 * Get a jQuery function within a specific document.
889 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
890 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
892 * @return {Function} Bound jQuery function
894 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
895 function wrapper( selector
) {
896 return $( selector
, wrapper
.context
);
899 wrapper
.context
= this.getDocument( context
);
902 wrapper
.$iframe
= $iframe
;
909 * Get the document of an element.
912 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
913 * @return {HTMLDocument|null} Document object
915 OO
.ui
.Element
.static.getDocument = function ( obj
) {
916 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
917 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
918 // Empty jQuery selections might have a context
925 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
930 * Get the window of an element or document.
933 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
934 * @return {Window} Window object
936 OO
.ui
.Element
.static.getWindow = function ( obj
) {
937 var doc
= this.getDocument( obj
);
938 return doc
.defaultView
;
942 * Get the direction of an element or document.
945 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
946 * @return {string} Text direction, either 'ltr' or 'rtl'
948 OO
.ui
.Element
.static.getDir = function ( obj
) {
951 if ( obj
instanceof jQuery
) {
954 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
955 isWin
= obj
.document
!== undefined;
956 if ( isDoc
|| isWin
) {
962 return $( obj
).css( 'direction' );
966 * Get the offset between two frames.
968 * TODO: Make this function not use recursion.
971 * @param {Window} from Window of the child frame
972 * @param {Window} [to=window] Window of the parent frame
973 * @param {Object} [offset] Offset to start with, used internally
974 * @return {Object} Offset object, containing left and top properties
976 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
977 var i
, len
, frames
, frame
, rect
;
983 offset
= { top
: 0, left
: 0 };
985 if ( from.parent
=== from ) {
989 // Get iframe element
990 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
991 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
992 if ( frames
[ i
].contentWindow
=== from ) {
998 // Recursively accumulate offset values
1000 rect
= frame
.getBoundingClientRect();
1001 offset
.left
+= rect
.left
;
1002 offset
.top
+= rect
.top
;
1003 if ( from !== to
) {
1004 this.getFrameOffset( from.parent
, offset
);
1011 * Get the offset between two elements.
1013 * The two elements may be in a different frame, but in that case the frame $element is in must
1014 * be contained in the frame $anchor is in.
1017 * @param {jQuery} $element Element whose position to get
1018 * @param {jQuery} $anchor Element to get $element's position relative to
1019 * @return {Object} Translated position coordinates, containing top and left properties
1021 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1022 var iframe
, iframePos
,
1023 pos
= $element
.offset(),
1024 anchorPos
= $anchor
.offset(),
1025 elementDocument
= this.getDocument( $element
),
1026 anchorDocument
= this.getDocument( $anchor
);
1028 // If $element isn't in the same document as $anchor, traverse up
1029 while ( elementDocument
!== anchorDocument
) {
1030 iframe
= elementDocument
.defaultView
.frameElement
;
1032 throw new Error( '$element frame is not contained in $anchor frame' );
1034 iframePos
= $( iframe
).offset();
1035 pos
.left
+= iframePos
.left
;
1036 pos
.top
+= iframePos
.top
;
1037 elementDocument
= iframe
.ownerDocument
;
1039 pos
.left
-= anchorPos
.left
;
1040 pos
.top
-= anchorPos
.top
;
1045 * Get element border sizes.
1048 * @param {HTMLElement} el Element to measure
1049 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1051 OO
.ui
.Element
.static.getBorders = function ( el
) {
1052 var doc
= el
.ownerDocument
,
1053 win
= doc
.defaultView
,
1054 style
= win
.getComputedStyle( el
, null ),
1056 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1057 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1058 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1059 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1070 * Get dimensions of an element or window.
1073 * @param {HTMLElement|Window} el Element to measure
1074 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1076 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1078 doc
= el
.ownerDocument
|| el
.document
,
1079 win
= doc
.defaultView
;
1081 if ( win
=== el
|| el
=== doc
.documentElement
) {
1084 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1086 top
: $win
.scrollTop(),
1087 left
: $win
.scrollLeft()
1089 scrollbar
: { right
: 0, bottom
: 0 },
1093 bottom
: $win
.innerHeight(),
1094 right
: $win
.innerWidth()
1100 borders
: this.getBorders( el
),
1102 top
: $el
.scrollTop(),
1103 left
: $el
.scrollLeft()
1106 right
: $el
.innerWidth() - el
.clientWidth
,
1107 bottom
: $el
.innerHeight() - el
.clientHeight
1109 rect
: el
.getBoundingClientRect()
1115 * Get the number of pixels that an element's content is scrolled to the left.
1117 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1118 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1120 * This function smooths out browser inconsistencies (nicely described in the README at
1121 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1122 * with Firefox's 'scrollLeft', which seems the sanest.
1126 * @param {HTMLElement|Window} el Element to measure
1127 * @return {number} Scroll position from the left.
1128 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1129 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1130 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1131 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1133 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1134 var rtlScrollType
= null;
1137 var $definer
= $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1138 definer
= $definer
[ 0 ];
1140 $definer
.appendTo( 'body' );
1141 if ( definer
.scrollLeft
> 0 ) {
1143 rtlScrollType
= 'default';
1145 definer
.scrollLeft
= 1;
1146 if ( definer
.scrollLeft
=== 0 ) {
1147 // Firefox, old Opera
1148 rtlScrollType
= 'negative';
1150 // Internet Explorer, Edge
1151 rtlScrollType
= 'reverse';
1157 return function getScrollLeft( el
) {
1158 var isRoot
= el
.window
=== el
||
1159 el
=== el
.ownerDocument
.body
||
1160 el
=== el
.ownerDocument
.documentElement
,
1161 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1162 // All browsers use the correct scroll type ('negative') on the root, so don't
1163 // do any fixups when looking at the root element
1164 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1166 if ( direction
=== 'rtl' ) {
1167 if ( rtlScrollType
=== null ) {
1170 if ( rtlScrollType
=== 'reverse' ) {
1171 scrollLeft
= -scrollLeft
;
1172 } else if ( rtlScrollType
=== 'default' ) {
1173 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1182 * Get the root scrollable element of given element's document.
1184 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1185 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1186 * lets us use 'body' or 'documentElement' based on what is working.
1188 * https://code.google.com/p/chromium/issues/detail?id=303131
1191 * @param {HTMLElement} el Element to find root scrollable parent for
1192 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1193 * depending on browser
1195 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1196 var scrollTop
, body
;
1198 if ( OO
.ui
.scrollableElement
=== undefined ) {
1199 body
= el
.ownerDocument
.body
;
1200 scrollTop
= body
.scrollTop
;
1203 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1204 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1205 if ( Math
.round( body
.scrollTop
) === 1 ) {
1206 body
.scrollTop
= scrollTop
;
1207 OO
.ui
.scrollableElement
= 'body';
1209 OO
.ui
.scrollableElement
= 'documentElement';
1213 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1217 * Get closest scrollable container.
1219 * Traverses up until either a scrollable element or the root is reached, in which case the root
1220 * scrollable element will be returned (see #getRootScrollableElement).
1223 * @param {HTMLElement} el Element to find scrollable container for
1224 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1225 * @return {HTMLElement} Closest scrollable container
1227 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1229 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1230 // 'overflow-y' have different values, so we need to check the separate properties.
1231 props
= [ 'overflow-x', 'overflow-y' ],
1232 $parent
= $( el
).parent();
1234 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1235 props
= [ 'overflow-' + dimension
];
1238 // Special case for the document root (which doesn't really have any scrollable container, since
1239 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1240 if ( $( el
).is( 'html, body' ) ) {
1241 return this.getRootScrollableElement( el
);
1244 while ( $parent
.length
) {
1245 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1246 return $parent
[ 0 ];
1250 val
= $parent
.css( props
[ i
] );
1251 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1252 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1253 // unintentionally perform a scroll in such case even if the application doesn't scroll
1254 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1255 // This could cause funny issues...
1256 if ( val
=== 'auto' || val
=== 'scroll' ) {
1257 return $parent
[ 0 ];
1260 $parent
= $parent
.parent();
1262 // The element is unattached... return something mostly sane
1263 return this.getRootScrollableElement( el
);
1267 * Scroll element into view.
1270 * @param {HTMLElement} el Element to scroll into view
1271 * @param {Object} [config] Configuration options
1272 * @param {string} [config.duration='fast'] jQuery animation duration value
1273 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1274 * to scroll in both directions
1275 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1277 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1278 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1279 deferred
= $.Deferred();
1281 // Configuration initialization
1282 config
= config
|| {};
1285 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1286 $container
= $( container
);
1287 elementDimensions
= this.getDimensions( el
);
1288 containerDimensions
= this.getDimensions( container
);
1289 $window
= $( this.getWindow( el
) );
1291 // Compute the element's position relative to the container
1292 if ( $container
.is( 'html, body' ) ) {
1293 // If the scrollable container is the root, this is easy
1295 top
: elementDimensions
.rect
.top
,
1296 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1297 left
: elementDimensions
.rect
.left
,
1298 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1301 // Otherwise, we have to subtract el's coordinates from container's coordinates
1303 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1304 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1305 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1306 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1310 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1311 if ( position
.top
< 0 ) {
1312 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1313 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1314 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1317 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1318 if ( position
.left
< 0 ) {
1319 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1320 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1321 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1324 if ( !$.isEmptyObject( animations
) ) {
1325 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1326 $container
.queue( function ( next
) {
1333 return deferred
.promise();
1337 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1338 * and reserve space for them, because it probably doesn't.
1340 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1341 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1342 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1343 * and then reattach (or show) them back.
1346 * @param {HTMLElement} el Element to reconsider the scrollbars on
1348 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1349 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1350 // Save scroll position
1351 scrollLeft
= el
.scrollLeft
;
1352 scrollTop
= el
.scrollTop
;
1353 // Detach all children
1354 while ( el
.firstChild
) {
1355 nodes
.push( el
.firstChild
);
1356 el
.removeChild( el
.firstChild
);
1359 void el
.offsetHeight
;
1360 // Reattach all children
1361 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1362 el
.appendChild( nodes
[ i
] );
1364 // Restore scroll position (no-op if scrollbars disappeared)
1365 el
.scrollLeft
= scrollLeft
;
1366 el
.scrollTop
= scrollTop
;
1372 * Toggle visibility of an element.
1374 * @param {boolean} [show] Make element visible, omit to toggle visibility
1378 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1379 show
= show
=== undefined ? !this.visible
: !!show
;
1381 if ( show
!== this.isVisible() ) {
1382 this.visible
= show
;
1383 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1384 this.emit( 'toggle', show
);
1391 * Check if element is visible.
1393 * @return {boolean} element is visible
1395 OO
.ui
.Element
.prototype.isVisible = function () {
1396 return this.visible
;
1402 * @return {Mixed} Element data
1404 OO
.ui
.Element
.prototype.getData = function () {
1411 * @param {Mixed} data Element data
1414 OO
.ui
.Element
.prototype.setData = function ( data
) {
1420 * Set the element has an 'id' attribute.
1422 * @param {string} id
1425 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1426 this.elementId
= id
;
1427 this.$element
.attr( 'id', id
);
1432 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1433 * and return its value.
1437 OO
.ui
.Element
.prototype.getElementId = function () {
1438 if ( this.elementId
=== null ) {
1439 this.setElementId( OO
.ui
.generateElementId() );
1441 return this.elementId
;
1445 * Check if element supports one or more methods.
1447 * @param {string|string[]} methods Method or list of methods to check
1448 * @return {boolean} All methods are supported
1450 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1454 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1455 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1456 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1461 return methods
.length
=== support
;
1465 * Update the theme-provided classes.
1467 * @localdoc This is called in element mixins and widget classes any time state changes.
1468 * Updating is debounced, minimizing overhead of changing multiple attributes and
1469 * guaranteeing that theme updates do not occur within an element's constructor
1471 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1472 OO
.ui
.theme
.queueUpdateElementClasses( this );
1476 * Get the HTML tag name.
1478 * Override this method to base the result on instance information.
1480 * @return {string} HTML tag name
1482 OO
.ui
.Element
.prototype.getTagName = function () {
1483 return this.constructor.static.tagName
;
1487 * Check if the element is attached to the DOM
1489 * @return {boolean} The element is attached to the DOM
1491 OO
.ui
.Element
.prototype.isElementAttached = function () {
1492 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1496 * Get the DOM document.
1498 * @return {HTMLDocument} Document object
1500 OO
.ui
.Element
.prototype.getElementDocument = function () {
1501 // Don't cache this in other ways either because subclasses could can change this.$element
1502 return OO
.ui
.Element
.static.getDocument( this.$element
);
1506 * Get the DOM window.
1508 * @return {Window} Window object
1510 OO
.ui
.Element
.prototype.getElementWindow = function () {
1511 return OO
.ui
.Element
.static.getWindow( this.$element
);
1515 * Get closest scrollable container.
1517 * @return {HTMLElement} Closest scrollable container
1519 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1520 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1524 * Get group element is in.
1526 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1528 OO
.ui
.Element
.prototype.getElementGroup = function () {
1529 return this.elementGroup
;
1533 * Set group element is in.
1535 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1538 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1539 this.elementGroup
= group
;
1544 * Scroll element into view.
1546 * @param {Object} [config] Configuration options
1547 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1549 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1551 !this.isElementAttached() ||
1552 !this.isVisible() ||
1553 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1555 return $.Deferred().resolve();
1557 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1561 * Restore the pre-infusion dynamic state for this widget.
1563 * This method is called after #$element has been inserted into DOM. The parameter is the return
1564 * value of #gatherPreInfuseState.
1567 * @param {Object} state
1569 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1573 * Wraps an HTML snippet for use with configuration values which default
1574 * to strings. This bypasses the default html-escaping done to string
1580 * @param {string} [content] HTML content
1582 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1584 this.content
= content
;
1589 OO
.initClass( OO
.ui
.HtmlSnippet
);
1596 * @return {string} Unchanged HTML snippet.
1598 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1599 return this.content
;
1603 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1604 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1605 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1606 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1607 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1611 * @extends OO.ui.Element
1612 * @mixins OO.EventEmitter
1615 * @param {Object} [config] Configuration options
1617 OO
.ui
.Layout
= function OoUiLayout( config
) {
1618 // Configuration initialization
1619 config
= config
|| {};
1621 // Parent constructor
1622 OO
.ui
.Layout
.parent
.call( this, config
);
1624 // Mixin constructors
1625 OO
.EventEmitter
.call( this );
1628 this.$element
.addClass( 'oo-ui-layout' );
1633 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1634 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1637 * Widgets are compositions of one or more OOUI elements that users can both view
1638 * and interact with. All widgets can be configured and modified via a standard API,
1639 * and their state can change dynamically according to a model.
1643 * @extends OO.ui.Element
1644 * @mixins OO.EventEmitter
1647 * @param {Object} [config] Configuration options
1648 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1649 * appearance reflects this state.
1651 OO
.ui
.Widget
= function OoUiWidget( config
) {
1652 // Initialize config
1653 config
= $.extend( { disabled
: false }, config
);
1655 // Parent constructor
1656 OO
.ui
.Widget
.parent
.call( this, config
);
1658 // Mixin constructors
1659 OO
.EventEmitter
.call( this );
1662 this.disabled
= null;
1663 this.wasDisabled
= null;
1666 this.$element
.addClass( 'oo-ui-widget' );
1667 this.setDisabled( !!config
.disabled
);
1672 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1673 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1680 * A 'disable' event is emitted when the disabled state of the widget changes
1681 * (i.e. on disable **and** enable).
1683 * @param {boolean} disabled Widget is disabled
1689 * A 'toggle' event is emitted when the visibility of the widget changes.
1691 * @param {boolean} visible Widget is visible
1697 * Check if the widget is disabled.
1699 * @return {boolean} Widget is disabled
1701 OO
.ui
.Widget
.prototype.isDisabled = function () {
1702 return this.disabled
;
1706 * Set the 'disabled' state of the widget.
1708 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1710 * @param {boolean} disabled Disable widget
1713 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1716 this.disabled
= !!disabled
;
1717 isDisabled
= this.isDisabled();
1718 if ( isDisabled
!== this.wasDisabled
) {
1719 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1720 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1721 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1722 this.emit( 'disable', isDisabled
);
1723 this.updateThemeClasses();
1725 this.wasDisabled
= isDisabled
;
1731 * Update the disabled state, in case of changes in parent widget.
1735 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1736 this.setDisabled( this.disabled
);
1741 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1744 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1747 * @return {string|null} The ID of the labelable element
1749 OO
.ui
.Widget
.prototype.getInputId = function () {
1754 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1755 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1756 * override this method to provide intuitive, accessible behavior.
1758 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1759 * Individual widgets may override it too.
1761 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1764 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1775 OO
.ui
.Theme
= function OoUiTheme() {
1776 this.elementClassesQueue
= [];
1777 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1782 OO
.initClass( OO
.ui
.Theme
);
1787 * Get a list of classes to be applied to a widget.
1789 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1790 * otherwise state transitions will not work properly.
1792 * @param {OO.ui.Element} element Element for which to get classes
1793 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1795 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1796 return { on
: [], off
: [] };
1800 * Update CSS classes provided by the theme.
1802 * For elements with theme logic hooks, this should be called any time there's a state change.
1804 * @param {OO.ui.Element} element Element for which to update classes
1806 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1807 var $elements
= $( [] ),
1808 classes
= this.getElementClasses( element
);
1810 if ( element
.$icon
) {
1811 $elements
= $elements
.add( element
.$icon
);
1813 if ( element
.$indicator
) {
1814 $elements
= $elements
.add( element
.$indicator
);
1818 .removeClass( classes
.off
.join( ' ' ) )
1819 .addClass( classes
.on
.join( ' ' ) );
1825 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1827 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1828 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1831 this.elementClassesQueue
= [];
1835 * Queue #updateElementClasses to be called for this element.
1837 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1838 * to make them synchronous.
1840 * @param {OO.ui.Element} element Element for which to update classes
1842 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1843 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1844 // the most common case (this method is often called repeatedly for the same element).
1845 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1848 this.elementClassesQueue
.push( element
);
1849 this.debouncedUpdateQueuedElementClasses();
1853 * Get the transition duration in milliseconds for dialogs opening/closing
1855 * The dialog should be fully rendered this many milliseconds after the
1856 * ready process has executed.
1858 * @return {number} Transition duration in milliseconds
1860 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1865 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1866 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1867 * order in which users will navigate through the focusable elements via the "tab" key.
1870 * // TabIndexedElement is mixed into the ButtonWidget class
1871 * // to provide a tabIndex property.
1872 * var button1 = new OO.ui.ButtonWidget( {
1876 * var button2 = new OO.ui.ButtonWidget( {
1880 * var button3 = new OO.ui.ButtonWidget( {
1884 * var button4 = new OO.ui.ButtonWidget( {
1888 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1894 * @param {Object} [config] Configuration options
1895 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1896 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1897 * functionality will be applied to it instead.
1898 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1899 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1900 * to remove the element from the tab-navigation flow.
1902 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1903 // Configuration initialization
1904 config
= $.extend( { tabIndex
: 0 }, config
);
1907 this.$tabIndexed
= null;
1908 this.tabIndex
= null;
1911 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1914 this.setTabIndex( config
.tabIndex
);
1915 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1920 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1925 * Set the element that should use the tabindex functionality.
1927 * This method is used to retarget a tabindex mixin so that its functionality applies
1928 * to the specified element. If an element is currently using the functionality, the mixin’s
1929 * effect on that element is removed before the new element is set up.
1931 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1934 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1935 var tabIndex
= this.tabIndex
;
1936 // Remove attributes from old $tabIndexed
1937 this.setTabIndex( null );
1938 // Force update of new $tabIndexed
1939 this.$tabIndexed
= $tabIndexed
;
1940 this.tabIndex
= tabIndex
;
1941 return this.updateTabIndex();
1945 * Set the value of the tabindex.
1947 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1950 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1951 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1953 if ( this.tabIndex
!== tabIndex
) {
1954 this.tabIndex
= tabIndex
;
1955 this.updateTabIndex();
1962 * Update the `tabindex` attribute, in case of changes to tab index or
1968 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1969 if ( this.$tabIndexed
) {
1970 if ( this.tabIndex
!== null ) {
1971 // Do not index over disabled elements
1972 this.$tabIndexed
.attr( {
1973 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1974 // Support: ChromeVox and NVDA
1975 // These do not seem to inherit aria-disabled from parent elements
1976 'aria-disabled': this.isDisabled().toString()
1979 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1986 * Handle disable events.
1989 * @param {boolean} disabled Element is disabled
1991 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1992 this.updateTabIndex();
1996 * Get the value of the tabindex.
1998 * @return {number|null} Tabindex value
2000 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2001 return this.tabIndex
;
2005 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2007 * If the element already has an ID then that is returned, otherwise unique ID is
2008 * generated, set on the element, and returned.
2010 * @return {string|null} The ID of the focusable element
2012 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2015 if ( !this.$tabIndexed
) {
2018 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2022 id
= this.$tabIndexed
.attr( 'id' );
2023 if ( id
=== undefined ) {
2024 id
= OO
.ui
.generateElementId();
2025 this.$tabIndexed
.attr( 'id', id
);
2032 * Whether the node is 'labelable' according to the HTML spec
2033 * (i.e., whether it can be interacted with through a `<label for="…">`).
2034 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2037 * @param {jQuery} $node
2040 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2042 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2043 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2045 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2048 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2055 * Focus this element.
2059 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2060 if ( !this.isDisabled() ) {
2061 this.$tabIndexed
.focus();
2067 * Blur this element.
2071 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2072 this.$tabIndexed
.blur();
2077 * @inheritdoc OO.ui.Widget
2079 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2084 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2085 * interface element that can be configured with access keys for accessibility.
2086 * See the [OOUI documentation on MediaWiki] [1] for examples.
2088 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2094 * @param {Object} [config] Configuration options
2095 * @cfg {jQuery} [$button] The button element created by the class.
2096 * If this configuration is omitted, the button element will use a generated `<a>`.
2097 * @cfg {boolean} [framed=true] Render the button with a frame
2099 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2100 // Configuration initialization
2101 config
= config
|| {};
2104 this.$button
= null;
2106 this.active
= config
.active
!== undefined && config
.active
;
2107 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
2108 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2109 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2110 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
2111 this.onClickHandler
= this.onClick
.bind( this );
2112 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2115 this.$element
.addClass( 'oo-ui-buttonElement' );
2116 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2117 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2122 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2124 /* Static Properties */
2127 * Cancel mouse down events.
2129 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2130 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2131 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2136 * @property {boolean}
2138 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2143 * A 'click' event is emitted when the button element is clicked.
2151 * Set the button element.
2153 * This method is used to retarget a button mixin so that its functionality applies to
2154 * the specified button element instead of the one created by the class. If a button element
2155 * is already set, the method will remove the mixin’s effect on that element.
2157 * @param {jQuery} $button Element to use as button
2159 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2160 if ( this.$button
) {
2162 .removeClass( 'oo-ui-buttonElement-button' )
2163 .removeAttr( 'role accesskey' )
2165 mousedown
: this.onMouseDownHandler
,
2166 keydown
: this.onKeyDownHandler
,
2167 click
: this.onClickHandler
,
2168 keypress
: this.onKeyPressHandler
2172 this.$button
= $button
2173 .addClass( 'oo-ui-buttonElement-button' )
2175 mousedown
: this.onMouseDownHandler
,
2176 keydown
: this.onKeyDownHandler
,
2177 click
: this.onClickHandler
,
2178 keypress
: this.onKeyPressHandler
2181 // Add `role="button"` on `<a>` elements, where it's needed
2182 // `toUppercase()` is added for XHTML documents
2183 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2184 this.$button
.attr( 'role', 'button' );
2189 * Handles mouse down events.
2192 * @param {jQuery.Event} e Mouse down event
2194 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2195 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2198 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2199 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2200 // reliably remove the pressed class
2201 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
2202 // Prevent change of focus unless specifically configured otherwise
2203 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2209 * Handles mouse up events.
2212 * @param {MouseEvent} e Mouse up event
2214 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
2215 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2218 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2219 // Stop listening for mouseup, since we only needed this once
2220 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
2224 * Handles mouse click events.
2227 * @param {jQuery.Event} e Mouse click event
2230 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2231 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2232 if ( this.emit( 'click' ) ) {
2239 * Handles key down events.
2242 * @param {jQuery.Event} e Key down event
2244 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2245 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2248 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2249 // Run the keyup handler no matter where the key is when the button is let go, so we can
2250 // reliably remove the pressed class
2251 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2255 * Handles key up events.
2258 * @param {KeyboardEvent} e Key up event
2260 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2261 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2264 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2265 // Stop listening for keyup, since we only needed this once
2266 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2270 * Handles key press events.
2273 * @param {jQuery.Event} e Key press event
2276 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2277 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2278 if ( this.emit( 'click' ) ) {
2285 * Check if button has a frame.
2287 * @return {boolean} Button is framed
2289 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2294 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2296 * @param {boolean} [framed] Make button framed, omit to toggle
2299 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2300 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2301 if ( framed
!== this.framed
) {
2302 this.framed
= framed
;
2304 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2305 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2306 this.updateThemeClasses();
2313 * Set the button's active state.
2315 * The active state can be set on:
2317 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2318 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2319 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2322 * @param {boolean} value Make button active
2325 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2326 this.active
= !!value
;
2327 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2328 this.updateThemeClasses();
2333 * Check if the button is active
2336 * @return {boolean} The button is active
2338 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2343 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2344 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2345 * items from the group is done through the interface the class provides.
2346 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2348 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2351 * @mixins OO.EmitterList
2355 * @param {Object} [config] Configuration options
2356 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2357 * is omitted, the group element will use a generated `<div>`.
2359 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2360 // Configuration initialization
2361 config
= config
|| {};
2363 // Mixin constructors
2364 OO
.EmitterList
.call( this, config
);
2370 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2375 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2382 * A change event is emitted when the set of selected items changes.
2384 * @param {OO.ui.Element[]} items Items currently in the group
2390 * Set the group element.
2392 * If an element is already set, items will be moved to the new element.
2394 * @param {jQuery} $group Element to use as group
2396 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2399 this.$group
= $group
;
2400 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2401 this.$group
.append( this.items
[ i
].$element
);
2406 * Find an item by its data.
2408 * Only the first item with matching data will be returned. To return all matching items,
2409 * use the #findItemsFromData method.
2411 * @param {Object} data Item data to search for
2412 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2414 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2416 hash
= OO
.getHash( data
);
2418 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2419 item
= this.items
[ i
];
2420 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2429 * Get an item by its data.
2431 * @deprecated Since v0.25.0; use {@link #findItemFromData} instead.
2432 * @param {Object} data Item data to search for
2433 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2435 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2436 OO
.ui
.warnDeprecation( 'GroupElement#getItemFromData. Deprecated function. Use findItemFromData instead. See T76630' );
2437 return this.findItemFromData( data
);
2441 * Find items by their data.
2443 * All items with matching data will be returned. To return only the first match, use the #findItemFromData method instead.
2445 * @param {Object} data Item data to search for
2446 * @return {OO.ui.Element[]} Items with equivalent data
2448 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2450 hash
= OO
.getHash( data
),
2453 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2454 item
= this.items
[ i
];
2455 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2464 * Find items by their data.
2466 * @deprecated Since v0.25.0; use {@link #findItemsFromData} instead.
2467 * @param {Object} data Item data to search for
2468 * @return {OO.ui.Element[]} Items with equivalent data
2470 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2471 OO
.ui
.warnDeprecation( 'GroupElement#getItemsFromData. Deprecated function. Use findItemsFromData instead. See T76630' );
2472 return this.findItemsFromData( data
);
2476 * Add items to the group.
2478 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2479 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2481 * @param {OO.ui.Element[]} items An array of items to add to the group
2482 * @param {number} [index] Index of the insertion point
2485 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2487 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2489 this.emit( 'change', this.getItems() );
2496 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2497 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2498 this.insertItemElements( items
, newIndex
);
2501 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2509 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2510 item
.setElementGroup( this );
2511 this.insertItemElements( item
, index
);
2514 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2520 * Insert elements into the group
2523 * @param {OO.ui.Element} itemWidget Item to insert
2524 * @param {number} index Insertion index
2526 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2527 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2528 this.$group
.append( itemWidget
.$element
);
2529 } else if ( index
=== 0 ) {
2530 this.$group
.prepend( itemWidget
.$element
);
2532 this.items
[ index
].$element
.before( itemWidget
.$element
);
2537 * Remove the specified items from a group.
2539 * Removed items are detached (not removed) from the DOM so that they may be reused.
2540 * To remove all items from a group, you may wish to use the #clearItems method instead.
2542 * @param {OO.ui.Element[]} items An array of items to remove
2545 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2546 var i
, len
, item
, index
;
2548 // Remove specific items elements
2549 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2551 index
= this.items
.indexOf( item
);
2552 if ( index
!== -1 ) {
2553 item
.setElementGroup( null );
2554 item
.$element
.detach();
2559 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2561 this.emit( 'change', this.getItems() );
2566 * Clear all items from the group.
2568 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2569 * To remove only a subset of items from a group, use the #removeItems method.
2573 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2576 // Remove all item elements
2577 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2578 this.items
[ i
].setElementGroup( null );
2579 this.items
[ i
].$element
.detach();
2583 OO
.EmitterList
.prototype.clearItems
.call( this );
2585 this.emit( 'change', this.getItems() );
2590 * IconElement is often mixed into other classes to generate an icon.
2591 * Icons are graphics, about the size of normal text. They are used to aid the user
2592 * in locating a control or to convey information in a space-efficient way. See the
2593 * [OOUI documentation on MediaWiki] [1] for a list of icons
2594 * included in the library.
2596 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2602 * @param {Object} [config] Configuration options
2603 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2604 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2605 * the icon element be set to an existing icon instead of the one generated by this class, set a
2606 * value using a jQuery selection. For example:
2608 * // Use a <div> tag instead of a <span>
2610 * // Use an existing icon element instead of the one generated by the class
2611 * $icon: this.$element
2612 * // Use an icon element from a child widget
2613 * $icon: this.childwidget.$element
2614 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2615 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2616 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2617 * by the user's language.
2619 * Example of an i18n map:
2621 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2622 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2623 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2624 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2625 * text. The icon title is displayed when users move the mouse over the icon.
2627 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2628 // Configuration initialization
2629 config
= config
|| {};
2634 this.iconTitle
= null;
2637 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2638 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2639 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2644 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2646 /* Static Properties */
2649 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2650 * for i18n purposes and contains a `default` icon name and additional names keyed by
2651 * language code. The `default` name is used when no icon is keyed by the user's language.
2653 * Example of an i18n map:
2655 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2657 * Note: the static property will be overridden if the #icon configuration is used.
2661 * @property {Object|string}
2663 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2666 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2667 * function that returns title text, or `null` for no title.
2669 * The static property will be overridden if the #iconTitle configuration is used.
2673 * @property {string|Function|null}
2675 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2680 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2681 * applies to the specified icon element instead of the one created by the class. If an icon
2682 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2683 * and mixin methods will no longer affect the element.
2685 * @param {jQuery} $icon Element to use as icon
2687 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2690 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2691 .removeAttr( 'title' );
2695 .addClass( 'oo-ui-iconElement-icon' )
2696 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2697 if ( this.iconTitle
!== null ) {
2698 this.$icon
.attr( 'title', this.iconTitle
);
2701 this.updateThemeClasses();
2705 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2706 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2709 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2710 * by language code, or `null` to remove the icon.
2713 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2714 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2715 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2717 if ( this.icon
!== icon
) {
2719 if ( this.icon
!== null ) {
2720 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2722 if ( icon
!== null ) {
2723 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2729 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2730 this.updateThemeClasses();
2736 * Set the icon title. Use `null` to remove the title.
2738 * @param {string|Function|null} iconTitle A text string used as the icon title,
2739 * a function that returns title text, or `null` for no title.
2742 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2744 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
2745 OO
.ui
.resolveMsg( iconTitle
) : null;
2747 if ( this.iconTitle
!== iconTitle
) {
2748 this.iconTitle
= iconTitle
;
2750 if ( this.iconTitle
!== null ) {
2751 this.$icon
.attr( 'title', iconTitle
);
2753 this.$icon
.removeAttr( 'title' );
2762 * Get the symbolic name of the icon.
2764 * @return {string} Icon name
2766 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2771 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2773 * @return {string} Icon title text
2775 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2776 return this.iconTitle
;
2780 * IndicatorElement is often mixed into other classes to generate an indicator.
2781 * Indicators are small graphics that are generally used in two ways:
2783 * - To draw attention to the status of an item. For example, an indicator might be
2784 * used to show that an item in a list has errors that need to be resolved.
2785 * - To clarify the function of a control that acts in an exceptional way (a button
2786 * that opens a menu instead of performing an action directly, for example).
2788 * For a list of indicators included in the library, please see the
2789 * [OOUI documentation on MediaWiki] [1].
2791 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2797 * @param {Object} [config] Configuration options
2798 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2799 * configuration is omitted, the indicator element will use a generated `<span>`.
2800 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2801 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
2803 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2804 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2805 * or a function that returns title text. The indicator title is displayed when users move
2806 * the mouse over the indicator.
2808 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2809 // Configuration initialization
2810 config
= config
|| {};
2813 this.$indicator
= null;
2814 this.indicator
= null;
2815 this.indicatorTitle
= null;
2818 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2819 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2820 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2825 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2827 /* Static Properties */
2830 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2831 * The static property will be overridden if the #indicator configuration is used.
2835 * @property {string|null}
2837 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2840 * A text string used as the indicator title, a function that returns title text, or `null`
2841 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2845 * @property {string|Function|null}
2847 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2852 * Set the indicator element.
2854 * If an element is already set, it will be cleaned up before setting up the new element.
2856 * @param {jQuery} $indicator Element to use as indicator
2858 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2859 if ( this.$indicator
) {
2861 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2862 .removeAttr( 'title' );
2865 this.$indicator
= $indicator
2866 .addClass( 'oo-ui-indicatorElement-indicator' )
2867 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2868 if ( this.indicatorTitle
!== null ) {
2869 this.$indicator
.attr( 'title', this.indicatorTitle
);
2872 this.updateThemeClasses();
2876 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
2878 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2881 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2882 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2884 if ( this.indicator
!== indicator
) {
2885 if ( this.$indicator
) {
2886 if ( this.indicator
!== null ) {
2887 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2889 if ( indicator
!== null ) {
2890 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2893 this.indicator
= indicator
;
2896 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2897 this.updateThemeClasses();
2903 * Set the indicator title.
2905 * The title is displayed when a user moves the mouse over the indicator.
2907 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2908 * `null` for no indicator title
2911 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2913 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
2914 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2916 if ( this.indicatorTitle
!== indicatorTitle
) {
2917 this.indicatorTitle
= indicatorTitle
;
2918 if ( this.$indicator
) {
2919 if ( this.indicatorTitle
!== null ) {
2920 this.$indicator
.attr( 'title', indicatorTitle
);
2922 this.$indicator
.removeAttr( 'title' );
2931 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2933 * @return {string} Symbolic name of indicator
2935 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2936 return this.indicator
;
2940 * Get the indicator title.
2942 * The title is displayed when a user moves the mouse over the indicator.
2944 * @return {string} Indicator title text
2946 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2947 return this.indicatorTitle
;
2951 * LabelElement is often mixed into other classes to generate a label, which
2952 * helps identify the function of an interface element.
2953 * See the [OOUI documentation on MediaWiki] [1] for more information.
2955 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2961 * @param {Object} [config] Configuration options
2962 * @cfg {jQuery} [$label] The label element created by the class. If this
2963 * configuration is omitted, the label element will use a generated `<span>`.
2964 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2965 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2966 * in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2967 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2969 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2970 // Configuration initialization
2971 config
= config
|| {};
2978 this.setLabel( config
.label
|| this.constructor.static.label
);
2979 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2984 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2989 * @event labelChange
2990 * @param {string} value
2993 /* Static Properties */
2996 * The label text. The label can be specified as a plaintext string, a function that will
2997 * produce a string in the future, or `null` for no label. The static value will
2998 * be overridden if a label is specified with the #label config option.
3002 * @property {string|Function|null}
3004 OO
.ui
.mixin
.LabelElement
.static.label
= null;
3006 /* Static methods */
3009 * Highlight the first occurrence of the query in the given text
3011 * @param {string} text Text
3012 * @param {string} query Query to find
3013 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3014 * @return {jQuery} Text with the first match of the query
3015 * sub-string wrapped in highlighted span
3017 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
3020 $result
= $( '<span>' );
3024 qLen
= query
.length
;
3025 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
3026 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
3031 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
3034 if ( !query
.length
|| offset
=== -1 ) {
3035 $result
.text( text
);
3038 document
.createTextNode( text
.slice( 0, offset
) ),
3040 .addClass( 'oo-ui-labelElement-label-highlight' )
3041 .text( text
.slice( offset
, offset
+ query
.length
) ),
3042 document
.createTextNode( text
.slice( offset
+ query
.length
) )
3045 return $result
.contents();
3051 * Set the label element.
3053 * If an element is already set, it will be cleaned up before setting up the new element.
3055 * @param {jQuery} $label Element to use as label
3057 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
3058 if ( this.$label
) {
3059 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
3062 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
3063 this.setLabelContent( this.label
);
3069 * An empty string will result in the label being hidden. A string containing only whitespace will
3070 * be converted to a single ` `.
3072 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
3073 * text; or null for no label
3076 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
3077 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
3078 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
3080 if ( this.label
!== label
) {
3081 if ( this.$label
) {
3082 this.setLabelContent( label
);
3085 this.emit( 'labelChange' );
3088 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
3094 * Set the label as plain text with a highlighted query
3096 * @param {string} text Text label to set
3097 * @param {string} query Substring of text to highlight
3098 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3101 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
3102 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
3108 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
3109 * text; or null for no label
3111 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
3116 * Set the content of the label.
3118 * Do not call this method until after the label element has been set by #setLabelElement.
3121 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
3122 * text; or null for no label
3124 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
3125 if ( typeof label
=== 'string' ) {
3126 if ( label
.match( /^\s*$/ ) ) {
3127 // Convert whitespace only string to a single non-breaking space
3128 this.$label
.html( ' ' );
3130 this.$label
.text( label
);
3132 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
3133 this.$label
.html( label
.toString() );
3134 } else if ( label
instanceof jQuery
) {
3135 this.$label
.empty().append( label
);
3137 this.$label
.empty();
3142 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3143 * additional functionality to an element created by another class. The class provides
3144 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3145 * which are used to customize the look and feel of a widget to better describe its
3146 * importance and functionality.
3148 * The library currently contains the following styling flags for general use:
3150 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3151 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3153 * The flags affect the appearance of the buttons:
3156 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3157 * var button1 = new OO.ui.ButtonWidget( {
3158 * label: 'Progressive',
3159 * flags: 'progressive'
3161 * var button2 = new OO.ui.ButtonWidget( {
3162 * label: 'Destructive',
3163 * flags: 'destructive'
3165 * $( 'body' ).append( button1.$element, button2.$element );
3167 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3168 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3170 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3176 * @param {Object} [config] Configuration options
3177 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3178 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3179 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3180 * @cfg {jQuery} [$flagged] The flagged element. By default,
3181 * the flagged functionality is applied to the element created by the class ($element).
3182 * If a different element is specified, the flagged functionality will be applied to it instead.
3184 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3185 // Configuration initialization
3186 config
= config
|| {};
3190 this.$flagged
= null;
3193 this.setFlags( config
.flags
);
3194 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3201 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3202 * parameter contains the name of each modified flag and indicates whether it was
3205 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3206 * that the flag was added, `false` that the flag was removed.
3212 * Set the flagged element.
3214 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3215 * If an element is already set, the method will remove the mixin’s effect on that element.
3217 * @param {jQuery} $flagged Element that should be flagged
3219 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3220 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3221 return 'oo-ui-flaggedElement-' + flag
;
3224 if ( this.$flagged
) {
3225 this.$flagged
.removeClass( classNames
);
3228 this.$flagged
= $flagged
.addClass( classNames
);
3232 * Check if the specified flag is set.
3234 * @param {string} flag Name of flag
3235 * @return {boolean} The flag is set
3237 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3238 // This may be called before the constructor, thus before this.flags is set
3239 return this.flags
&& ( flag
in this.flags
);
3243 * Get the names of all flags set.
3245 * @return {string[]} Flag names
3247 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3248 // This may be called before the constructor, thus before this.flags is set
3249 return Object
.keys( this.flags
|| {} );
3258 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3259 var flag
, className
,
3262 classPrefix
= 'oo-ui-flaggedElement-';
3264 for ( flag
in this.flags
) {
3265 className
= classPrefix
+ flag
;
3266 changes
[ flag
] = false;
3267 delete this.flags
[ flag
];
3268 remove
.push( className
);
3271 if ( this.$flagged
) {
3272 this.$flagged
.removeClass( remove
.join( ' ' ) );
3275 this.updateThemeClasses();
3276 this.emit( 'flag', changes
);
3282 * Add one or more flags.
3284 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3285 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3286 * be added (`true`) or removed (`false`).
3290 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3291 var i
, len
, flag
, className
,
3295 classPrefix
= 'oo-ui-flaggedElement-';
3297 if ( typeof flags
=== 'string' ) {
3298 className
= classPrefix
+ flags
;
3300 if ( !this.flags
[ flags
] ) {
3301 this.flags
[ flags
] = true;
3302 add
.push( className
);
3304 } else if ( Array
.isArray( flags
) ) {
3305 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3307 className
= classPrefix
+ flag
;
3309 if ( !this.flags
[ flag
] ) {
3310 changes
[ flag
] = true;
3311 this.flags
[ flag
] = true;
3312 add
.push( className
);
3315 } else if ( OO
.isPlainObject( flags
) ) {
3316 for ( flag
in flags
) {
3317 className
= classPrefix
+ flag
;
3318 if ( flags
[ flag
] ) {
3320 if ( !this.flags
[ flag
] ) {
3321 changes
[ flag
] = true;
3322 this.flags
[ flag
] = true;
3323 add
.push( className
);
3327 if ( this.flags
[ flag
] ) {
3328 changes
[ flag
] = false;
3329 delete this.flags
[ flag
];
3330 remove
.push( className
);
3336 if ( this.$flagged
) {
3338 .addClass( add
.join( ' ' ) )
3339 .removeClass( remove
.join( ' ' ) );
3342 this.updateThemeClasses();
3343 this.emit( 'flag', changes
);
3349 * TitledElement is mixed into other classes to provide a `title` attribute.
3350 * Titles are rendered by the browser and are made visible when the user moves
3351 * the mouse over the element. Titles are not visible on touch devices.
3354 * // TitledElement provides a 'title' attribute to the
3355 * // ButtonWidget class
3356 * var button = new OO.ui.ButtonWidget( {
3357 * label: 'Button with Title',
3358 * title: 'I am a button'
3360 * $( 'body' ).append( button.$element );
3366 * @param {Object} [config] Configuration options
3367 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3368 * If this config is omitted, the title functionality is applied to $element, the
3369 * element created by the class.
3370 * @cfg {string|Function} [title] The title text or a function that returns text. If
3371 * this config is omitted, the value of the {@link #static-title static title} property is used.
3373 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3374 // Configuration initialization
3375 config
= config
|| {};
3378 this.$titled
= null;
3382 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3383 this.setTitledElement( config
.$titled
|| this.$element
);
3388 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3390 /* Static Properties */
3393 * The title text, a function that returns text, or `null` for no title. The value of the static property
3394 * is overridden if the #title config option is used.
3398 * @property {string|Function|null}
3400 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3405 * Set the titled element.
3407 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3408 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3410 * @param {jQuery} $titled Element that should use the 'titled' functionality
3412 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3413 if ( this.$titled
) {
3414 this.$titled
.removeAttr( 'title' );
3417 this.$titled
= $titled
;
3426 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3429 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3430 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3431 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3433 if ( this.title
!== title
) {
3442 * Update the title attribute, in case of changes to title or accessKey.
3447 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3448 var title
= this.getTitle();
3449 if ( this.$titled
) {
3450 if ( title
!== null ) {
3451 // Only if this is an AccessKeyedElement
3452 if ( this.formatTitleWithAccessKey
) {
3453 title
= this.formatTitleWithAccessKey( title
);
3455 this.$titled
.attr( 'title', title
);
3457 this.$titled
.removeAttr( 'title' );
3466 * @return {string} Title string
3468 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3473 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3474 * Accesskeys allow an user to go to a specific element by using
3475 * a shortcut combination of a browser specific keys + the key
3479 * // AccessKeyedElement provides an 'accesskey' attribute to the
3480 * // ButtonWidget class
3481 * var button = new OO.ui.ButtonWidget( {
3482 * label: 'Button with Accesskey',
3485 * $( 'body' ).append( button.$element );
3491 * @param {Object} [config] Configuration options
3492 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3493 * If this config is omitted, the accesskey functionality is applied to $element, the
3494 * element created by the class.
3495 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3496 * this config is omitted, no accesskey will be added.
3498 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3499 // Configuration initialization
3500 config
= config
|| {};
3503 this.$accessKeyed
= null;
3504 this.accessKey
= null;
3507 this.setAccessKey( config
.accessKey
|| null );
3508 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3510 // If this is also a TitledElement and it initialized before we did, we may have
3511 // to update the title with the access key
3512 if ( this.updateTitle
) {
3519 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3521 /* Static Properties */
3524 * The access key, a function that returns a key, or `null` for no accesskey.
3528 * @property {string|Function|null}
3530 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3535 * Set the accesskeyed element.
3537 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3538 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3540 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3542 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3543 if ( this.$accessKeyed
) {
3544 this.$accessKeyed
.removeAttr( 'accesskey' );
3547 this.$accessKeyed
= $accessKeyed
;
3548 if ( this.accessKey
) {
3549 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3556 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3559 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3560 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3562 if ( this.accessKey
!== accessKey
) {
3563 if ( this.$accessKeyed
) {
3564 if ( accessKey
!== null ) {
3565 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3567 this.$accessKeyed
.removeAttr( 'accesskey' );
3570 this.accessKey
= accessKey
;
3572 // Only if this is a TitledElement
3573 if ( this.updateTitle
) {
3584 * @return {string} accessKey string
3586 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3587 return this.accessKey
;
3591 * Add information about the access key to the element's tooltip label.
3592 * (This is only public for hacky usage in FieldLayout.)
3594 * @param {string} title Tooltip label for `title` attribute
3597 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3600 if ( !this.$accessKeyed
) {
3601 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3604 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3605 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3606 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3608 accessKey
= this.getAccessKey();
3611 title
+= ' [' + accessKey
+ ']';
3617 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3618 * feels, and functionality can be customized via the class’s configuration options
3619 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3622 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3625 * // A button widget
3626 * var button = new OO.ui.ButtonWidget( {
3627 * label: 'Button with Icon',
3629 * iconTitle: 'Remove'
3631 * $( 'body' ).append( button.$element );
3633 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3636 * @extends OO.ui.Widget
3637 * @mixins OO.ui.mixin.ButtonElement
3638 * @mixins OO.ui.mixin.IconElement
3639 * @mixins OO.ui.mixin.IndicatorElement
3640 * @mixins OO.ui.mixin.LabelElement
3641 * @mixins OO.ui.mixin.TitledElement
3642 * @mixins OO.ui.mixin.FlaggedElement
3643 * @mixins OO.ui.mixin.TabIndexedElement
3644 * @mixins OO.ui.mixin.AccessKeyedElement
3647 * @param {Object} [config] Configuration options
3648 * @cfg {boolean} [active=false] Whether button should be shown as active
3649 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3650 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3651 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3653 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3654 // Configuration initialization
3655 config
= config
|| {};
3657 // Parent constructor
3658 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3660 // Mixin constructors
3661 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3662 OO
.ui
.mixin
.IconElement
.call( this, config
);
3663 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3664 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3665 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3666 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3667 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3668 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3673 this.noFollow
= false;
3676 this.connect( this, { disable
: 'onDisable' } );
3679 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3681 .addClass( 'oo-ui-buttonWidget' )
3682 .append( this.$button
);
3683 this.setActive( config
.active
);
3684 this.setHref( config
.href
);
3685 this.setTarget( config
.target
);
3686 this.setNoFollow( config
.noFollow
);
3691 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3692 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3693 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3694 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3695 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3696 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3697 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3698 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3699 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3701 /* Static Properties */
3707 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3713 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3718 * Get hyperlink location.
3720 * @return {string} Hyperlink location
3722 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3727 * Get hyperlink target.
3729 * @return {string} Hyperlink target
3731 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3736 * Get search engine traversal hint.
3738 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3740 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3741 return this.noFollow
;
3745 * Set hyperlink location.
3747 * @param {string|null} href Hyperlink location, null to remove
3749 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3750 href
= typeof href
=== 'string' ? href
: null;
3751 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3755 if ( href
!== this.href
) {
3764 * Update the `href` attribute, in case of changes to href or
3770 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3771 if ( this.href
!== null && !this.isDisabled() ) {
3772 this.$button
.attr( 'href', this.href
);
3774 this.$button
.removeAttr( 'href' );
3781 * Handle disable events.
3784 * @param {boolean} disabled Element is disabled
3786 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3791 * Set hyperlink target.
3793 * @param {string|null} target Hyperlink target, null to remove
3795 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3796 target
= typeof target
=== 'string' ? target
: null;
3798 if ( target
!== this.target
) {
3799 this.target
= target
;
3800 if ( target
!== null ) {
3801 this.$button
.attr( 'target', target
);
3803 this.$button
.removeAttr( 'target' );
3811 * Set search engine traversal hint.
3813 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3815 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3816 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3818 if ( noFollow
!== this.noFollow
) {
3819 this.noFollow
= noFollow
;
3821 this.$button
.attr( 'rel', 'nofollow' );
3823 this.$button
.removeAttr( 'rel' );
3830 // Override method visibility hints from ButtonElement
3841 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3842 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3843 * removed, and cleared from the group.
3846 * // Example: A ButtonGroupWidget with two buttons
3847 * var button1 = new OO.ui.PopupButtonWidget( {
3848 * label: 'Select a category',
3851 * $content: $( '<p>List of categories...</p>' ),
3856 * var button2 = new OO.ui.ButtonWidget( {
3859 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3860 * items: [button1, button2]
3862 * $( 'body' ).append( buttonGroup.$element );
3865 * @extends OO.ui.Widget
3866 * @mixins OO.ui.mixin.GroupElement
3869 * @param {Object} [config] Configuration options
3870 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3872 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3873 // Configuration initialization
3874 config
= config
|| {};
3876 // Parent constructor
3877 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3879 // Mixin constructors
3880 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3883 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3884 if ( Array
.isArray( config
.items
) ) {
3885 this.addItems( config
.items
);
3891 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3892 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3894 /* Static Properties */
3900 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3909 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3910 if ( !this.isDisabled() ) {
3911 if ( this.items
[ 0 ] ) {
3912 this.items
[ 0 ].focus();
3921 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
3926 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3927 * which creates a label that identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
3928 * for a list of icons included in the library.
3931 * // An icon widget with a label
3932 * var myIcon = new OO.ui.IconWidget( {
3936 * // Create a label.
3937 * var iconLabel = new OO.ui.LabelWidget( {
3940 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3942 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
3945 * @extends OO.ui.Widget
3946 * @mixins OO.ui.mixin.IconElement
3947 * @mixins OO.ui.mixin.TitledElement
3948 * @mixins OO.ui.mixin.FlaggedElement
3951 * @param {Object} [config] Configuration options
3953 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3954 // Configuration initialization
3955 config
= config
|| {};
3957 // Parent constructor
3958 OO
.ui
.IconWidget
.parent
.call( this, config
);
3960 // Mixin constructors
3961 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3962 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3963 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3966 this.$element
.addClass( 'oo-ui-iconWidget' );
3971 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3972 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3973 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3974 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3976 /* Static Properties */
3982 OO
.ui
.IconWidget
.static.tagName
= 'span';
3985 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3986 * attention to the status of an item or to clarify the function within a control. For a list of
3987 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
3990 * // Example of an indicator widget
3991 * var indicator1 = new OO.ui.IndicatorWidget( {
3992 * indicator: 'required'
3995 * // Create a fieldset layout to add a label
3996 * var fieldset = new OO.ui.FieldsetLayout();
3997 * fieldset.addItems( [
3998 * new OO.ui.FieldLayout( indicator1, { label: 'A required indicator:' } )
4000 * $( 'body' ).append( fieldset.$element );
4002 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4005 * @extends OO.ui.Widget
4006 * @mixins OO.ui.mixin.IndicatorElement
4007 * @mixins OO.ui.mixin.TitledElement
4010 * @param {Object} [config] Configuration options
4012 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4013 // Configuration initialization
4014 config
= config
|| {};
4016 // Parent constructor
4017 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4019 // Mixin constructors
4020 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
4021 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4024 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4029 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4030 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4031 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4033 /* Static Properties */
4039 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4042 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4043 * be configured with a `label` option that is set to a string, a label node, or a function:
4045 * - String: a plaintext string
4046 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4047 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4048 * - Function: a function that will produce a string in the future. Functions are used
4049 * in cases where the value of the label is not currently defined.
4051 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4052 * will come into focus when the label is clicked.
4055 * // Examples of LabelWidgets
4056 * var label1 = new OO.ui.LabelWidget( {
4057 * label: 'plaintext label'
4059 * var label2 = new OO.ui.LabelWidget( {
4060 * label: $( '<a href="default.html">jQuery label</a>' )
4062 * // Create a fieldset layout with fields for each example
4063 * var fieldset = new OO.ui.FieldsetLayout();
4064 * fieldset.addItems( [
4065 * new OO.ui.FieldLayout( label1 ),
4066 * new OO.ui.FieldLayout( label2 )
4068 * $( 'body' ).append( fieldset.$element );
4071 * @extends OO.ui.Widget
4072 * @mixins OO.ui.mixin.LabelElement
4073 * @mixins OO.ui.mixin.TitledElement
4076 * @param {Object} [config] Configuration options
4077 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4078 * Clicking the label will focus the specified input field.
4080 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4081 // Configuration initialization
4082 config
= config
|| {};
4084 // Parent constructor
4085 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4087 // Mixin constructors
4088 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4089 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4092 this.input
= config
.input
;
4096 if ( this.input
.getInputId() ) {
4097 this.$element
.attr( 'for', this.input
.getInputId() );
4099 this.$label
.on( 'click', function () {
4100 this.input
.simulateLabelClick();
4104 this.$element
.addClass( 'oo-ui-labelWidget' );
4109 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4110 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4111 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4113 /* Static Properties */
4119 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4122 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4123 * and that they should wait before proceeding. The pending state is visually represented with a pending
4124 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4125 * field of a {@link OO.ui.TextInputWidget text input widget}.
4127 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4128 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4129 * in process dialogs.
4132 * function MessageDialog( config ) {
4133 * MessageDialog.parent.call( this, config );
4135 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4137 * MessageDialog.static.name = 'myMessageDialog';
4138 * MessageDialog.static.actions = [
4139 * { action: 'save', label: 'Done', flags: 'primary' },
4140 * { label: 'Cancel', flags: 'safe' }
4143 * MessageDialog.prototype.initialize = function () {
4144 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4145 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
4146 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
4147 * this.$body.append( this.content.$element );
4149 * MessageDialog.prototype.getBodyHeight = function () {
4152 * MessageDialog.prototype.getActionProcess = function ( action ) {
4153 * var dialog = this;
4154 * if ( action === 'save' ) {
4155 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4156 * return new OO.ui.Process()
4158 * .next( function () {
4159 * dialog.getActions().get({actions: 'save'})[0].popPending();
4162 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4165 * var windowManager = new OO.ui.WindowManager();
4166 * $( 'body' ).append( windowManager.$element );
4168 * var dialog = new MessageDialog();
4169 * windowManager.addWindows( [ dialog ] );
4170 * windowManager.openWindow( dialog );
4176 * @param {Object} [config] Configuration options
4177 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4179 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4180 // Configuration initialization
4181 config
= config
|| {};
4185 this.$pending
= null;
4188 this.setPendingElement( config
.$pending
|| this.$element
);
4193 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4198 * Set the pending element (and clean up any existing one).
4200 * @param {jQuery} $pending The element to set to pending.
4202 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4203 if ( this.$pending
) {
4204 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4207 this.$pending
= $pending
;
4208 if ( this.pending
> 0 ) {
4209 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4214 * Check if an element is pending.
4216 * @return {boolean} Element is pending
4218 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4219 return !!this.pending
;
4223 * Increase the pending counter. The pending state will remain active until the counter is zero
4224 * (i.e., the number of calls to #pushPending and #popPending is the same).
4228 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4229 if ( this.pending
=== 0 ) {
4230 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4231 this.updateThemeClasses();
4239 * Decrease the pending counter. The pending state will remain active until the counter is zero
4240 * (i.e., the number of calls to #pushPending and #popPending is the same).
4244 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4245 if ( this.pending
=== 1 ) {
4246 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4247 this.updateThemeClasses();
4249 this.pending
= Math
.max( 0, this.pending
- 1 );
4255 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4256 * in the document (for example, in an OO.ui.Window's $overlay).
4258 * The elements's position is automatically calculated and maintained when window is resized or the
4259 * page is scrolled. If you reposition the container manually, you have to call #position to make
4260 * sure the element is still placed correctly.
4262 * As positioning is only possible when both the element and the container are attached to the DOM
4263 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4264 * the #toggle method to display a floating popup, for example.
4270 * @param {Object} [config] Configuration options
4271 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4272 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4273 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4274 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4275 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4276 * 'top': Align the top edge with $floatableContainer's top edge
4277 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4278 * 'center': Vertically align the center with $floatableContainer's center
4279 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4280 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4281 * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4282 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4283 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4284 * 'center': Horizontally align the center with $floatableContainer's center
4285 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4288 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4289 // Configuration initialization
4290 config
= config
|| {};
4293 this.$floatable
= null;
4294 this.$floatableContainer
= null;
4295 this.$floatableWindow
= null;
4296 this.$floatableClosestScrollable
= null;
4297 this.floatableOutOfView
= false;
4298 this.onFloatableScrollHandler
= this.position
.bind( this );
4299 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4302 this.setFloatableContainer( config
.$floatableContainer
);
4303 this.setFloatableElement( config
.$floatable
|| this.$element
);
4304 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4305 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4306 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4312 * Set floatable element.
4314 * If an element is already set, it will be cleaned up before setting up the new element.
4316 * @param {jQuery} $floatable Element to make floatable
4318 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4319 if ( this.$floatable
) {
4320 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4321 this.$floatable
.css( { left
: '', top
: '' } );
4324 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4329 * Set floatable container.
4331 * The element will be positioned relative to the specified container.
4333 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4335 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4336 this.$floatableContainer
= $floatableContainer
;
4337 if ( this.$floatable
) {
4343 * Change how the element is positioned vertically.
4345 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4347 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4348 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4349 throw new Error( 'Invalid value for vertical position: ' + position
);
4351 if ( this.verticalPosition
!== position
) {
4352 this.verticalPosition
= position
;
4353 if ( this.$floatable
) {
4360 * Change how the element is positioned horizontally.
4362 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4364 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4365 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4366 throw new Error( 'Invalid value for horizontal position: ' + position
);
4368 if ( this.horizontalPosition
!== position
) {
4369 this.horizontalPosition
= position
;
4370 if ( this.$floatable
) {
4377 * Toggle positioning.
4379 * Do not turn positioning on until after the element is attached to the DOM and visible.
4381 * @param {boolean} [positioning] Enable positioning, omit to toggle
4384 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4385 var closestScrollableOfContainer
;
4387 if ( !this.$floatable
|| !this.$floatableContainer
) {
4391 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4393 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4394 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4395 this.warnedUnattached
= true;
4398 if ( this.positioning
!== positioning
) {
4399 this.positioning
= positioning
;
4401 this.needsCustomPosition
=
4402 this.verticalPostion
!== 'below' ||
4403 this.horizontalPosition
!== 'start' ||
4404 !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4406 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4407 // If the scrollable is the root, we have to listen to scroll events
4408 // on the window because of browser inconsistencies.
4409 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4410 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4413 if ( positioning
) {
4414 this.$floatableWindow
= $( this.getElementWindow() );
4415 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4417 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4418 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4420 // Initial position after visible
4423 if ( this.$floatableWindow
) {
4424 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4425 this.$floatableWindow
= null;
4428 if ( this.$floatableClosestScrollable
) {
4429 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4430 this.$floatableClosestScrollable
= null;
4433 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4441 * Check whether the bottom edge of the given element is within the viewport of the given container.
4444 * @param {jQuery} $element
4445 * @param {jQuery} $container
4448 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4449 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4450 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4451 direction
= $element
.css( 'direction' );
4453 elemRect
= $element
[ 0 ].getBoundingClientRect();
4454 if ( $container
[ 0 ] === window
) {
4455 viewportSpacing
= OO
.ui
.getViewportSpacing();
4459 right
: document
.documentElement
.clientWidth
,
4460 bottom
: document
.documentElement
.clientHeight
4462 contRect
.top
+= viewportSpacing
.top
;
4463 contRect
.left
+= viewportSpacing
.left
;
4464 contRect
.right
-= viewportSpacing
.right
;
4465 contRect
.bottom
-= viewportSpacing
.bottom
;
4467 contRect
= $container
[ 0 ].getBoundingClientRect();
4470 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4471 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4472 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4473 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4474 if ( direction
=== 'rtl' ) {
4475 startEdgeInBounds
= rightEdgeInBounds
;
4476 endEdgeInBounds
= leftEdgeInBounds
;
4478 startEdgeInBounds
= leftEdgeInBounds
;
4479 endEdgeInBounds
= rightEdgeInBounds
;
4482 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4485 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4488 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4491 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4495 // The other positioning values are all about being inside the container,
4496 // so in those cases all we care about is that any part of the container is visible.
4497 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4498 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4502 * Check if the floatable is hidden to the user because it was offscreen.
4504 * @return {boolean} Floatable is out of view
4506 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4507 return this.floatableOutOfView
;
4511 * Position the floatable below its container.
4513 * This should only be done when both of them are attached to the DOM and visible.
4517 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4518 if ( !this.positioning
) {
4523 // To continue, some things need to be true:
4524 // The element must actually be in the DOM
4525 this.isElementAttached() && (
4526 // The closest scrollable is the current window
4527 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4528 // OR is an element in the element's DOM
4529 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4532 // Abort early if important parts of the widget are no longer attached to the DOM
4536 this.floatableOutOfView
= this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4537 if ( this.floatableOutOfView
) {
4538 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4541 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4544 if ( !this.needsCustomPosition
) {
4548 this.$floatable
.css( this.computePosition() );
4550 // We updated the position, so re-evaluate the clipping state.
4551 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4552 // will not notice the need to update itself.)
4553 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4554 // it not listen to the right events in the right places?
4563 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4564 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4565 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4567 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4569 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4570 var isBody
, scrollableX
, scrollableY
, containerPos
,
4571 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4572 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4573 direction
= this.$floatableContainer
.css( 'direction' ),
4574 $offsetParent
= this.$floatable
.offsetParent();
4576 if ( $offsetParent
.is( 'html' ) ) {
4577 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4578 // <html> element, but they do work on the <body>
4579 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4581 isBody
= $offsetParent
.is( 'body' );
4582 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4583 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4585 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4586 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4587 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4588 // or if it isn't scrollable
4589 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4590 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4592 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4593 // if the <body> has a margin
4594 containerPos
= isBody
?
4595 this.$floatableContainer
.offset() :
4596 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4597 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4598 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4599 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4600 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4602 if ( this.verticalPosition
=== 'below' ) {
4603 newPos
.top
= containerPos
.bottom
;
4604 } else if ( this.verticalPosition
=== 'above' ) {
4605 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4606 } else if ( this.verticalPosition
=== 'top' ) {
4607 newPos
.top
= containerPos
.top
;
4608 } else if ( this.verticalPosition
=== 'bottom' ) {
4609 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4610 } else if ( this.verticalPosition
=== 'center' ) {
4611 newPos
.top
= containerPos
.top
+
4612 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4615 if ( this.horizontalPosition
=== 'before' ) {
4616 newPos
.end
= containerPos
.start
;
4617 } else if ( this.horizontalPosition
=== 'after' ) {
4618 newPos
.start
= containerPos
.end
;
4619 } else if ( this.horizontalPosition
=== 'start' ) {
4620 newPos
.start
= containerPos
.start
;
4621 } else if ( this.horizontalPosition
=== 'end' ) {
4622 newPos
.end
= containerPos
.end
;
4623 } else if ( this.horizontalPosition
=== 'center' ) {
4624 newPos
.left
= containerPos
.left
+
4625 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4628 if ( newPos
.start
!== undefined ) {
4629 if ( direction
=== 'rtl' ) {
4630 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4632 newPos
.left
= newPos
.start
;
4634 delete newPos
.start
;
4636 if ( newPos
.end
!== undefined ) {
4637 if ( direction
=== 'rtl' ) {
4638 newPos
.left
= newPos
.end
;
4640 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4645 // Account for scroll position
4646 if ( newPos
.top
!== '' ) {
4647 newPos
.top
+= scrollTop
;
4649 if ( newPos
.bottom
!== '' ) {
4650 newPos
.bottom
-= scrollTop
;
4652 if ( newPos
.left
!== '' ) {
4653 newPos
.left
+= scrollLeft
;
4655 if ( newPos
.right
!== '' ) {
4656 newPos
.right
-= scrollLeft
;
4659 // Account for scrollbar gutter
4660 if ( newPos
.bottom
!== '' ) {
4661 newPos
.bottom
-= horizScrollbarHeight
;
4663 if ( direction
=== 'rtl' ) {
4664 if ( newPos
.left
!== '' ) {
4665 newPos
.left
-= vertScrollbarWidth
;
4668 if ( newPos
.right
!== '' ) {
4669 newPos
.right
-= vertScrollbarWidth
;
4677 * Element that can be automatically clipped to visible boundaries.
4679 * Whenever the element's natural height changes, you have to call
4680 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4681 * clipping correctly.
4683 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4684 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4685 * then #$clippable will be given a fixed reduced height and/or width and will be made
4686 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4687 * but you can build a static footer by setting #$clippableContainer to an element that contains
4688 * #$clippable and the footer.
4694 * @param {Object} [config] Configuration options
4695 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4696 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4697 * omit to use #$clippable
4699 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4700 // Configuration initialization
4701 config
= config
|| {};
4704 this.$clippable
= null;
4705 this.$clippableContainer
= null;
4706 this.clipping
= false;
4707 this.clippedHorizontally
= false;
4708 this.clippedVertically
= false;
4709 this.$clippableScrollableContainer
= null;
4710 this.$clippableScroller
= null;
4711 this.$clippableWindow
= null;
4712 this.idealWidth
= null;
4713 this.idealHeight
= null;
4714 this.onClippableScrollHandler
= this.clip
.bind( this );
4715 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4718 if ( config
.$clippableContainer
) {
4719 this.setClippableContainer( config
.$clippableContainer
);
4721 this.setClippableElement( config
.$clippable
|| this.$element
);
4727 * Set clippable element.
4729 * If an element is already set, it will be cleaned up before setting up the new element.
4731 * @param {jQuery} $clippable Element to make clippable
4733 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4734 if ( this.$clippable
) {
4735 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4736 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4737 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4740 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4745 * Set clippable container.
4747 * This is the container that will be measured when deciding whether to clip. When clipping,
4748 * #$clippable will be resized in order to keep the clippable container fully visible.
4750 * If the clippable container is unset, #$clippable will be used.
4752 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4754 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4755 this.$clippableContainer
= $clippableContainer
;
4756 if ( this.$clippable
) {
4764 * Do not turn clipping on until after the element is attached to the DOM and visible.
4766 * @param {boolean} [clipping] Enable clipping, omit to toggle
4769 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4770 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4772 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4773 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4774 this.warnedUnattached
= true;
4777 if ( this.clipping
!== clipping
) {
4778 this.clipping
= clipping
;
4780 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4781 // If the clippable container is the root, we have to listen to scroll events and check
4782 // jQuery.scrollTop on the window because of browser inconsistencies
4783 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4784 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4785 this.$clippableScrollableContainer
;
4786 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4787 this.$clippableWindow
= $( this.getElementWindow() )
4788 .on( 'resize', this.onClippableWindowResizeHandler
);
4789 // Initial clip after visible
4792 this.$clippable
.css( {
4800 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4802 this.$clippableScrollableContainer
= null;
4803 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4804 this.$clippableScroller
= null;
4805 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4806 this.$clippableWindow
= null;
4814 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4816 * @return {boolean} Element will be clipped to the visible area
4818 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4819 return this.clipping
;
4823 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4825 * @return {boolean} Part of the element is being clipped
4827 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4828 return this.clippedHorizontally
|| this.clippedVertically
;
4832 * Check if the right of the element is being clipped by the nearest scrollable container.
4834 * @return {boolean} Part of the element is being clipped
4836 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4837 return this.clippedHorizontally
;
4841 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4843 * @return {boolean} Part of the element is being clipped
4845 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4846 return this.clippedVertically
;
4850 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4852 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4853 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4855 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4856 this.idealWidth
= width
;
4857 this.idealHeight
= height
;
4859 if ( !this.clipping
) {
4860 // Update dimensions
4861 this.$clippable
.css( { width
: width
, height
: height
} );
4863 // While clipping, idealWidth and idealHeight are not considered
4867 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4868 * ClippableElement will clip the opposite side when reducing element's width.
4870 * Classes that mix in ClippableElement should override this to return 'right' if their
4871 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
4872 * If your class also mixes in FloatableElement, this is handled automatically.
4874 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4875 * always in pixels, even if they were unset or set to 'auto'.)
4877 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
4879 * @return {string} 'left' or 'right'
4881 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
4882 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
4889 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4890 * ClippableElement will clip the opposite side when reducing element's width.
4892 * Classes that mix in ClippableElement should override this to return 'bottom' if their
4893 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
4894 * If your class also mixes in FloatableElement, this is handled automatically.
4896 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4897 * always in pixels, even if they were unset or set to 'auto'.)
4899 * When in doubt, 'top' is a sane fallback.
4901 * @return {string} 'top' or 'bottom'
4903 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
4904 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
4911 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4912 * when the element's natural height changes.
4914 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4915 * overlapped by, the visible area of the nearest scrollable container.
4917 * Because calling clip() when the natural height changes isn't always possible, we also set
4918 * max-height when the element isn't being clipped. This means that if the element tries to grow
4919 * beyond the edge, something reasonable will happen before clip() is called.
4923 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4924 var extraHeight
, extraWidth
, viewportSpacing
,
4925 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4926 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4927 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
4928 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
4929 // Extra tolerance so that the sloppy code below doesn't result in results that are off
4930 // by one or two pixels. (And also so that we have space to display drop shadows.)
4931 // Chosen by fair dice roll.
4934 if ( !this.clipping
) {
4935 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4939 function rectIntersection( a
, b
) {
4941 out
.top
= Math
.max( a
.top
, b
.top
);
4942 out
.left
= Math
.max( a
.left
, b
.left
);
4943 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
4944 out
.right
= Math
.min( a
.right
, b
.right
);
4948 viewportSpacing
= OO
.ui
.getViewportSpacing();
4950 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4951 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
4952 // Dimensions of the browser window, rather than the element!
4956 right
: document
.documentElement
.clientWidth
,
4957 bottom
: document
.documentElement
.clientHeight
4959 viewportRect
.top
+= viewportSpacing
.top
;
4960 viewportRect
.left
+= viewportSpacing
.left
;
4961 viewportRect
.right
-= viewportSpacing
.right
;
4962 viewportRect
.bottom
-= viewportSpacing
.bottom
;
4964 $viewport
= this.$clippableScrollableContainer
;
4965 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
4966 // Convert into a plain object
4967 viewportRect
= $.extend( {}, viewportRect
);
4970 // Account for scrollbar gutter
4971 direction
= $viewport
.css( 'direction' );
4972 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
4973 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
4974 viewportRect
.bottom
-= horizScrollbarHeight
;
4975 if ( direction
=== 'rtl' ) {
4976 viewportRect
.left
+= vertScrollbarWidth
;
4978 viewportRect
.right
-= vertScrollbarWidth
;
4981 // Add arbitrary tolerance
4982 viewportRect
.top
+= buffer
;
4983 viewportRect
.left
+= buffer
;
4984 viewportRect
.right
-= buffer
;
4985 viewportRect
.bottom
-= buffer
;
4987 $item
= this.$clippableContainer
|| this.$clippable
;
4989 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
4990 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
4992 itemRect
= $item
[ 0 ].getBoundingClientRect();
4993 // Convert into a plain object
4994 itemRect
= $.extend( {}, itemRect
);
4996 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
4997 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
4998 if ( this.getHorizontalAnchorEdge() === 'right' ) {
4999 itemRect
.left
= viewportRect
.left
;
5001 itemRect
.right
= viewportRect
.right
;
5003 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5004 itemRect
.top
= viewportRect
.top
;
5006 itemRect
.bottom
= viewportRect
.bottom
;
5009 availableRect
= rectIntersection( viewportRect
, itemRect
);
5011 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5012 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5013 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5014 desiredWidth
= Math
.min( desiredWidth
,
5015 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5016 desiredHeight
= Math
.min( desiredHeight
,
5017 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5018 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5019 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5020 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5021 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5022 clipWidth
= allotedWidth
< naturalWidth
;
5023 clipHeight
= allotedHeight
< naturalHeight
;
5026 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5027 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5028 this.$clippable
.css( 'overflowX', 'scroll' );
5029 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5030 this.$clippable
.css( {
5031 width
: Math
.max( 0, allotedWidth
),
5035 this.$clippable
.css( {
5037 width
: this.idealWidth
|| '',
5038 maxWidth
: Math
.max( 0, allotedWidth
)
5042 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5043 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5044 this.$clippable
.css( 'overflowY', 'scroll' );
5045 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5046 this.$clippable
.css( {
5047 height
: Math
.max( 0, allotedHeight
),
5051 this.$clippable
.css( {
5053 height
: this.idealHeight
|| '',
5054 maxHeight
: Math
.max( 0, allotedHeight
)
5058 // If we stopped clipping in at least one of the dimensions
5059 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5060 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5063 this.clippedHorizontally
= clipWidth
;
5064 this.clippedVertically
= clipHeight
;
5070 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5071 * By default, each popup has an anchor that points toward its origin.
5072 * Please see the [OOUI documentation on Mediawiki] [1] for more information and examples.
5074 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5077 * // A popup widget.
5078 * var popup = new OO.ui.PopupWidget( {
5079 * $content: $( '<p>Hi there!</p>' ),
5084 * $( 'body' ).append( popup.$element );
5085 * // To display the popup, toggle the visibility to 'true'.
5086 * popup.toggle( true );
5088 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5091 * @extends OO.ui.Widget
5092 * @mixins OO.ui.mixin.LabelElement
5093 * @mixins OO.ui.mixin.ClippableElement
5094 * @mixins OO.ui.mixin.FloatableElement
5097 * @param {Object} [config] Configuration options
5098 * @cfg {number} [width=320] Width of popup in pixels
5099 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
5100 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5101 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5102 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5103 * of $floatableContainer
5104 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5105 * of $floatableContainer
5106 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5107 * endwards (right/left) to the vertical center of $floatableContainer
5108 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5109 * startwards (left/right) to the vertical center of $floatableContainer
5110 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5111 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5112 * as possible while still keeping the anchor within the popup;
5113 * if position is before/after, move the popup as far downwards as possible.
5114 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5115 * as possible while still keeping the anchor within the popup;
5116 * if position in before/after, move the popup as far upwards as possible.
5117 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5118 * of the popup with the center of $floatableContainer.
5119 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5120 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5121 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5122 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5123 * desired direction to display the popup without clipping
5124 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5125 * See the [OOUI docs on MediaWiki][3] for an example.
5126 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5127 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5128 * @cfg {jQuery} [$content] Content to append to the popup's body
5129 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5130 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5131 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5132 * This config option is only relevant if #autoClose is set to `true`. See the [OOUI documentation on MediaWiki][2]
5134 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5135 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5137 * @cfg {boolean} [padded=false] Add padding to the popup's body
5139 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5140 // Configuration initialization
5141 config
= config
|| {};
5143 // Parent constructor
5144 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5146 // Properties (must be set before ClippableElement constructor call)
5147 this.$body
= $( '<div>' );
5148 this.$popup
= $( '<div>' );
5150 // Mixin constructors
5151 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5152 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5153 $clippable
: this.$body
,
5154 $clippableContainer
: this.$popup
5156 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5159 this.$anchor
= $( '<div>' );
5160 // If undefined, will be computed lazily in computePosition()
5161 this.$container
= config
.$container
;
5162 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5163 this.autoClose
= !!config
.autoClose
;
5164 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
5165 this.transitionTimeout
= null;
5166 this.anchored
= false;
5167 this.width
= config
.width
!== undefined ? config
.width
: 320;
5168 this.height
= config
.height
!== undefined ? config
.height
: null;
5169 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
5170 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5173 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5174 this.setAlignment( config
.align
|| 'center' );
5175 this.setPosition( config
.position
|| 'below' );
5176 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5177 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5178 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5180 .addClass( 'oo-ui-popupWidget-popup' )
5181 .append( this.$body
);
5183 .addClass( 'oo-ui-popupWidget' )
5184 .append( this.$popup
, this.$anchor
);
5185 // Move content, which was added to #$element by OO.ui.Widget, to the body
5186 // FIXME This is gross, we should use '$body' or something for the config
5187 if ( config
.$content
instanceof jQuery
) {
5188 this.$body
.append( config
.$content
);
5191 if ( config
.padded
) {
5192 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5195 if ( config
.head
) {
5196 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5197 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5198 this.$head
= $( '<div>' )
5199 .addClass( 'oo-ui-popupWidget-head' )
5200 .append( this.$label
, this.closeButton
.$element
);
5201 this.$popup
.prepend( this.$head
);
5204 if ( config
.$footer
) {
5205 this.$footer
= $( '<div>' )
5206 .addClass( 'oo-ui-popupWidget-footer' )
5207 .append( config
.$footer
);
5208 this.$popup
.append( this.$footer
);
5211 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5212 // that reference properties not initialized at that time of parent class construction
5213 // TODO: Find a better way to handle post-constructor setup
5214 this.visible
= false;
5215 this.$element
.addClass( 'oo-ui-element-hidden' );
5220 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5221 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5222 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5223 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5230 * The popup is ready: it is visible and has been positioned and clipped.
5236 * Handles mouse down events.
5239 * @param {MouseEvent} e Mouse down event
5241 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
5244 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5246 this.toggle( false );
5251 * Bind mouse down listener.
5255 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5256 // Capture clicks outside popup
5257 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
5261 * Handles close button click events.
5265 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5266 if ( this.isVisible() ) {
5267 this.toggle( false );
5272 * Unbind mouse down listener.
5276 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5277 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
5281 * Handles key down events.
5284 * @param {KeyboardEvent} e Key down event
5286 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5288 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5291 this.toggle( false );
5293 e
.stopPropagation();
5298 * Bind key down listener.
5302 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5303 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5307 * Unbind key down listener.
5311 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5312 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5316 * Show, hide, or toggle the visibility of the anchor.
5318 * @param {boolean} [show] Show anchor, omit to toggle
5320 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5321 show
= show
=== undefined ? !this.anchored
: !!show
;
5323 if ( this.anchored
!== show
) {
5325 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5326 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5328 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5329 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5331 this.anchored
= show
;
5336 * Change which edge the anchor appears on.
5338 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5340 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5341 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5342 throw new Error( 'Invalid value for edge: ' + edge
);
5344 if ( this.anchorEdge
!== null ) {
5345 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5347 this.anchorEdge
= edge
;
5348 if ( this.anchored
) {
5349 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5354 * Check if the anchor is visible.
5356 * @return {boolean} Anchor is visible
5358 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5359 return this.anchored
;
5363 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5364 * `.toggle( true )` after its #$element is attached to the DOM.
5366 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5367 * it in the right place and with the right dimensions only work correctly while it is attached.
5368 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5369 * strictly enforced, so currently it only generates a warning in the browser console.
5374 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5375 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5376 show
= show
=== undefined ? !this.isVisible() : !!show
;
5378 change
= show
!== this.isVisible();
5380 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5381 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5382 this.warnedUnattached
= true;
5384 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5385 // Fall back to the parent node if the floatableContainer is not set
5386 this.setFloatableContainer( this.$element
.parent() );
5389 if ( change
&& show
&& this.autoFlip
) {
5390 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5391 // (e.g. if the user scrolled).
5392 this.isAutoFlipped
= false;
5396 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5399 this.togglePositioning( show
&& !!this.$floatableContainer
);
5402 if ( this.autoClose
) {
5403 this.bindMouseDownListener();
5404 this.bindKeyDownListener();
5406 this.updateDimensions();
5407 this.toggleClipping( true );
5409 if ( this.autoFlip
) {
5410 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5411 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5412 // If opening the popup in the normal direction causes it to be clipped, open
5413 // in the opposite one instead
5414 normalHeight
= this.$element
.height();
5415 this.isAutoFlipped
= !this.isAutoFlipped
;
5417 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5418 // If that also causes it to be clipped, open in whichever direction
5419 // we have more space
5420 oppositeHeight
= this.$element
.height();
5421 if ( oppositeHeight
< normalHeight
) {
5422 this.isAutoFlipped
= !this.isAutoFlipped
;
5428 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5429 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5430 // If opening the popup in the normal direction causes it to be clipped, open
5431 // in the opposite one instead
5432 normalWidth
= this.$element
.width();
5433 this.isAutoFlipped
= !this.isAutoFlipped
;
5434 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5435 // which causes positioning to be off. Toggle clipping back and fort to work around.
5436 this.toggleClipping( false );
5438 this.toggleClipping( true );
5439 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5440 // If that also causes it to be clipped, open in whichever direction
5441 // we have more space
5442 oppositeWidth
= this.$element
.width();
5443 if ( oppositeWidth
< normalWidth
) {
5444 this.isAutoFlipped
= !this.isAutoFlipped
;
5445 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5446 // which causes positioning to be off. Toggle clipping back and fort to work around.
5447 this.toggleClipping( false );
5449 this.toggleClipping( true );
5456 this.emit( 'ready' );
5458 this.toggleClipping( false );
5459 if ( this.autoClose
) {
5460 this.unbindMouseDownListener();
5461 this.unbindKeyDownListener();
5470 * Set the size of the popup.
5472 * Changing the size may also change the popup's position depending on the alignment.
5474 * @param {number} width Width in pixels
5475 * @param {number} height Height in pixels
5476 * @param {boolean} [transition=false] Use a smooth transition
5479 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5481 this.height
= height
!== undefined ? height
: null;
5482 if ( this.isVisible() ) {
5483 this.updateDimensions( transition
);
5488 * Update the size and position.
5490 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5491 * be called automatically.
5493 * @param {boolean} [transition=false] Use a smooth transition
5496 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5499 // Prevent transition from being interrupted
5500 clearTimeout( this.transitionTimeout
);
5502 // Enable transition
5503 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5509 // Prevent transitioning after transition is complete
5510 this.transitionTimeout
= setTimeout( function () {
5511 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5514 // Prevent transitioning immediately
5515 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5522 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5523 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5524 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5525 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5527 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5528 popupPositionOppositeMap
= {
5536 'force-left': 'backwards',
5537 'force-right': 'forwards'
5540 'force-left': 'forwards',
5541 'force-right': 'backwards'
5553 backwards
: this.anchored
? 'before' : 'end'
5561 if ( !this.$container
) {
5562 // Lazy-initialize $container if not specified in constructor
5563 this.$container
= $( this.getClosestScrollableElementContainer() );
5565 direction
= this.$container
.css( 'direction' );
5567 // Set height and width before we do anything else, since it might cause our measurements
5568 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5571 height
: this.height
!== null ? this.height
: 'auto'
5574 align
= alignMap
[ direction
][ this.align
] || this.align
;
5575 popupPosition
= this.popupPosition
;
5576 if ( this.isAutoFlipped
) {
5577 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5580 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5581 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5582 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5583 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5584 near
= vertical
? 'top' : 'left';
5585 far
= vertical
? 'bottom' : 'right';
5586 sizeProp
= vertical
? 'Height' : 'Width';
5587 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : this.width
;
5589 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5590 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5591 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5594 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5595 // Find out which property FloatableElement used for positioning, and adjust that value
5596 positionProp
= vertical
?
5597 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5598 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5600 // Figure out where the near and far edges of the popup and $floatableContainer are
5601 floatablePos
= this.$floatableContainer
.offset();
5602 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5603 // Measure where the offsetParent is and compute our position based on that and parentPosition
5604 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5605 { top
: 0, left
: 0 } :
5606 this.$element
.offsetParent().offset();
5608 if ( positionProp
=== near
) {
5609 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5610 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5612 popupPos
[ far
] = offsetParentPos
[ near
] +
5613 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5614 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5617 if ( this.anchored
) {
5618 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5619 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5620 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5622 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5623 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5624 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5625 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5626 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5627 // Not enough space for the anchor on the start side; pull the popup startwards
5628 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5629 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5630 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5631 // Not enough space for the anchor on the end side; pull the popup endwards
5632 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5633 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5635 positionAdjustment
= 0;
5638 positionAdjustment
= 0;
5641 // Check if the popup will go beyond the edge of this.$container
5642 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5643 { top
: 0, left
: 0 } :
5644 this.$container
.offset();
5645 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5646 if ( this.$container
[ 0 ] === document
.documentElement
) {
5647 viewportSpacing
= OO
.ui
.getViewportSpacing();
5648 containerPos
[ near
] += viewportSpacing
[ near
];
5649 containerPos
[ far
] -= viewportSpacing
[ far
];
5651 // Take into account how much the popup will move because of the adjustments we're going to make
5652 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5653 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5654 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5655 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5656 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5657 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5658 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5659 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5660 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5661 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5664 if ( this.anchored
) {
5665 // Adjust anchorOffset for positionAdjustment
5666 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5668 // Position the anchor
5669 anchorCss
[ start
] = anchorOffset
;
5670 this.$anchor
.css( anchorCss
);
5673 // Move the popup if needed
5674 parentPosition
[ positionProp
] += positionAdjustment
;
5676 return parentPosition
;
5680 * Set popup alignment
5682 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5683 * `backwards` or `forwards`.
5685 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5686 // Validate alignment
5687 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5690 this.align
= 'center';
5696 * Get popup alignment
5698 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5699 * `backwards` or `forwards`.
5701 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5706 * Change the positioning of the popup.
5708 * @param {string} position 'above', 'below', 'before' or 'after'
5710 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5711 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5714 this.popupPosition
= position
;
5719 * Get popup positioning.
5721 * @return {string} 'above', 'below', 'before' or 'after'
5723 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5724 return this.popupPosition
;
5728 * Set popup auto-flipping.
5730 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5731 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5732 * desired direction to display the popup without clipping
5734 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5735 autoFlip
= !!autoFlip
;
5737 if ( this.autoFlip
!== autoFlip
) {
5738 this.autoFlip
= autoFlip
;
5743 * Get an ID of the body element, this can be used as the
5744 * `aria-describedby` attribute for an input field.
5746 * @return {string} The ID of the body element
5748 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5749 var id
= this.$body
.attr( 'id' );
5750 if ( id
=== undefined ) {
5751 id
= OO
.ui
.generateElementId();
5752 this.$body
.attr( 'id', id
);
5758 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5759 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5760 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5761 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5767 * @param {Object} [config] Configuration options
5768 * @cfg {Object} [popup] Configuration to pass to popup
5769 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5771 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5772 // Configuration initialization
5773 config
= config
|| {};
5776 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5779 $floatableContainer
: this.$element
5783 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5793 * @return {OO.ui.PopupWidget} Popup widget
5795 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5800 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5801 * which is used to display additional information or options.
5804 * // Example of a popup button.
5805 * var popupButton = new OO.ui.PopupButtonWidget( {
5806 * label: 'Popup button with options',
5809 * $content: $( '<p>Additional options here.</p>' ),
5811 * align: 'force-left'
5814 * // Append the button to the DOM.
5815 * $( 'body' ).append( popupButton.$element );
5818 * @extends OO.ui.ButtonWidget
5819 * @mixins OO.ui.mixin.PopupElement
5822 * @param {Object} [config] Configuration options
5823 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5824 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5825 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5826 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
5828 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5829 // Configuration initialization
5830 config
= config
|| {};
5832 // Parent constructor
5833 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5835 // Mixin constructors
5836 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5839 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
5842 this.connect( this, { click
: 'onAction' } );
5846 .addClass( 'oo-ui-popupButtonWidget' )
5847 .attr( 'aria-haspopup', 'true' );
5849 .addClass( 'oo-ui-popupButtonWidget-popup' )
5850 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5851 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5852 this.$overlay
.append( this.popup
.$element
);
5857 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
5858 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
5863 * Handle the button action being triggered.
5867 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5868 this.popup
.toggle();
5872 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5874 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5879 * @mixins OO.ui.mixin.GroupElement
5882 * @param {Object} [config] Configuration options
5884 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5885 // Mixin constructors
5886 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5891 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5896 * Set the disabled state of the widget.
5898 * This will also update the disabled state of child widgets.
5900 * @param {boolean} disabled Disable widget
5903 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5907 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5908 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5910 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5912 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5913 this.items
[ i
].updateDisabled();
5921 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5923 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5924 * allows bidirectional communication.
5926 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5934 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5941 * Check if widget is disabled.
5943 * Checks parent if present, making disabled state inheritable.
5945 * @return {boolean} Widget is disabled
5947 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5948 return this.disabled
||
5949 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5953 * Set group element is in.
5955 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5958 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5960 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5961 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5963 // Initialize item disabled states
5964 this.updateDisabled();
5970 * OptionWidgets are special elements that can be selected and configured with data. The
5971 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5972 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5973 * and examples, please see the [OOUI documentation on MediaWiki][1].
5975 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
5978 * @extends OO.ui.Widget
5979 * @mixins OO.ui.mixin.ItemWidget
5980 * @mixins OO.ui.mixin.LabelElement
5981 * @mixins OO.ui.mixin.FlaggedElement
5982 * @mixins OO.ui.mixin.AccessKeyedElement
5985 * @param {Object} [config] Configuration options
5987 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5988 // Configuration initialization
5989 config
= config
|| {};
5991 // Parent constructor
5992 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5994 // Mixin constructors
5995 OO
.ui
.mixin
.ItemWidget
.call( this );
5996 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5997 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
5998 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6001 this.selected
= false;
6002 this.highlighted
= false;
6003 this.pressed
= false;
6007 .data( 'oo-ui-optionWidget', this )
6008 // Allow programmatic focussing (and by accesskey), but not tabbing
6009 .attr( 'tabindex', '-1' )
6010 .attr( 'role', 'option' )
6011 .attr( 'aria-selected', 'false' )
6012 .addClass( 'oo-ui-optionWidget' )
6013 .append( this.$label
);
6018 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6019 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6020 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6021 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6022 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6024 /* Static Properties */
6027 * Whether this option can be selected. See #setSelected.
6031 * @property {boolean}
6033 OO
.ui
.OptionWidget
.static.selectable
= true;
6036 * Whether this option can be highlighted. See #setHighlighted.
6040 * @property {boolean}
6042 OO
.ui
.OptionWidget
.static.highlightable
= true;
6045 * Whether this option can be pressed. See #setPressed.
6049 * @property {boolean}
6051 OO
.ui
.OptionWidget
.static.pressable
= true;
6054 * Whether this option will be scrolled into view when it is selected.
6058 * @property {boolean}
6060 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6065 * Check if the option can be selected.
6067 * @return {boolean} Item is selectable
6069 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6070 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6074 * Check if the option can be highlighted. A highlight indicates that the option
6075 * may be selected when a user presses enter or clicks. Disabled items cannot
6078 * @return {boolean} Item is highlightable
6080 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6081 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6085 * Check if the option can be pressed. The pressed state occurs when a user mouses
6086 * down on an item, but has not yet let go of the mouse.
6088 * @return {boolean} Item is pressable
6090 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6091 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6095 * Check if the option is selected.
6097 * @return {boolean} Item is selected
6099 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6100 return this.selected
;
6104 * Check if the option is highlighted. A highlight indicates that the
6105 * item may be selected when a user presses enter or clicks.
6107 * @return {boolean} Item is highlighted
6109 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6110 return this.highlighted
;
6114 * Check if the option is pressed. The pressed state occurs when a user mouses
6115 * down on an item, but has not yet let go of the mouse. The item may appear
6116 * selected, but it will not be selected until the user releases the mouse.
6118 * @return {boolean} Item is pressed
6120 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6121 return this.pressed
;
6125 * Set the option’s selected state. In general, all modifications to the selection
6126 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6127 * method instead of this method.
6129 * @param {boolean} [state=false] Select option
6132 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6133 if ( this.constructor.static.selectable
) {
6134 this.selected
= !!state
;
6136 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6137 .attr( 'aria-selected', state
.toString() );
6138 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6139 this.scrollElementIntoView();
6141 this.updateThemeClasses();
6147 * Set the option’s highlighted state. In general, all programmatic
6148 * modifications to the highlight should be handled by the
6149 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6150 * method instead of this method.
6152 * @param {boolean} [state=false] Highlight option
6155 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6156 if ( this.constructor.static.highlightable
) {
6157 this.highlighted
= !!state
;
6158 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6159 this.updateThemeClasses();
6165 * Set the option’s pressed state. In general, all
6166 * programmatic modifications to the pressed state should be handled by the
6167 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6168 * method instead of this method.
6170 * @param {boolean} [state=false] Press option
6173 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6174 if ( this.constructor.static.pressable
) {
6175 this.pressed
= !!state
;
6176 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6177 this.updateThemeClasses();
6183 * Get text to match search strings against.
6185 * The default implementation returns the label text, but subclasses
6186 * can override this to provide more complex behavior.
6188 * @return {string|boolean} String to match search string against
6190 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6191 var label
= this.getLabel();
6192 return typeof label
=== 'string' ? label
: this.$label
.text();
6196 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6197 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6198 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6201 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6202 * information, please see the [OOUI documentation on MediaWiki][1].
6205 * // Example of a select widget with three options
6206 * var select = new OO.ui.SelectWidget( {
6208 * new OO.ui.OptionWidget( {
6210 * label: 'Option One',
6212 * new OO.ui.OptionWidget( {
6214 * label: 'Option Two',
6216 * new OO.ui.OptionWidget( {
6218 * label: 'Option Three',
6222 * $( 'body' ).append( select.$element );
6224 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6228 * @extends OO.ui.Widget
6229 * @mixins OO.ui.mixin.GroupWidget
6232 * @param {Object} [config] Configuration options
6233 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6234 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6235 * the [OOUI documentation on MediaWiki] [2] for examples.
6236 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6238 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6239 // Configuration initialization
6240 config
= config
|| {};
6242 // Parent constructor
6243 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6245 // Mixin constructors
6246 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6249 this.pressed
= false;
6250 this.selecting
= null;
6251 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
6252 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
6253 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
6254 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
6255 this.keyPressBuffer
= '';
6256 this.keyPressBufferTimer
= null;
6257 this.blockMouseOverEvents
= 0;
6260 this.connect( this, {
6264 focusin
: this.onFocus
.bind( this ),
6265 mousedown
: this.onMouseDown
.bind( this ),
6266 mouseover
: this.onMouseOver
.bind( this ),
6267 mouseleave
: this.onMouseLeave
.bind( this )
6272 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6273 .attr( 'role', 'listbox' );
6274 this.setFocusOwner( this.$element
);
6275 if ( Array
.isArray( config
.items
) ) {
6276 this.addItems( config
.items
);
6282 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6283 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6290 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6292 * @param {OO.ui.OptionWidget|null} item Highlighted item
6298 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6299 * pressed state of an option.
6301 * @param {OO.ui.OptionWidget|null} item Pressed item
6307 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6309 * @param {OO.ui.OptionWidget|null} item Selected item
6314 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6315 * @param {OO.ui.OptionWidget} item Chosen item
6321 * An `add` event is emitted when options are added to the select with the #addItems method.
6323 * @param {OO.ui.OptionWidget[]} items Added items
6324 * @param {number} index Index of insertion point
6330 * A `remove` event is emitted when options are removed from the select with the #clearItems
6331 * or #removeItems methods.
6333 * @param {OO.ui.OptionWidget[]} items Removed items
6339 * Handle focus events
6342 * @param {jQuery.Event} event
6344 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6346 if ( event
.target
=== this.$element
[ 0 ] ) {
6347 // This widget was focussed, e.g. by the user tabbing to it.
6348 // The styles for focus state depend on one of the items being selected.
6349 if ( !this.findSelectedItem() ) {
6350 item
= this.findFirstSelectableItem();
6353 if ( event
.target
.tabIndex
=== -1 ) {
6354 // One of the options got focussed (and the event bubbled up here).
6355 // They can't be tabbed to, but they can be activated using accesskeys.
6356 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6357 item
= this.findTargetItem( event
);
6359 // There is something actually user-focusable in one of the labels of the options, and the
6360 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6366 if ( item
.constructor.static.highlightable
) {
6367 this.highlightItem( item
);
6369 this.selectItem( item
);
6373 if ( event
.target
!== this.$element
[ 0 ] ) {
6374 this.$focusOwner
.focus();
6379 * Handle mouse down events.
6382 * @param {jQuery.Event} e Mouse down event
6384 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6387 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6388 this.togglePressed( true );
6389 item
= this.findTargetItem( e
);
6390 if ( item
&& item
.isSelectable() ) {
6391 this.pressItem( item
);
6392 this.selecting
= item
;
6393 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
6394 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6401 * Handle mouse up events.
6404 * @param {MouseEvent} e Mouse up event
6406 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
6409 this.togglePressed( false );
6410 if ( !this.selecting
) {
6411 item
= this.findTargetItem( e
);
6412 if ( item
&& item
.isSelectable() ) {
6413 this.selecting
= item
;
6416 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6417 this.pressItem( null );
6418 this.chooseItem( this.selecting
);
6419 this.selecting
= null;
6422 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
6423 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6429 * Handle mouse move events.
6432 * @param {MouseEvent} e Mouse move event
6434 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
6437 if ( !this.isDisabled() && this.pressed
) {
6438 item
= this.findTargetItem( e
);
6439 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6440 this.pressItem( item
);
6441 this.selecting
= item
;
6447 * Handle mouse over events.
6450 * @param {jQuery.Event} e Mouse over event
6452 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6454 if ( this.blockMouseOverEvents
) {
6457 if ( !this.isDisabled() ) {
6458 item
= this.findTargetItem( e
);
6459 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6465 * Handle mouse leave events.
6468 * @param {jQuery.Event} e Mouse over event
6470 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6471 if ( !this.isDisabled() ) {
6472 this.highlightItem( null );
6478 * Handle key down events.
6481 * @param {KeyboardEvent} e Key down event
6483 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
6486 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6488 if ( !this.isDisabled() && this.isVisible() ) {
6489 switch ( e
.keyCode
) {
6490 case OO
.ui
.Keys
.ENTER
:
6491 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6492 // Was only highlighted, now let's select it. No-op if already selected.
6493 this.chooseItem( currentItem
);
6498 case OO
.ui
.Keys
.LEFT
:
6499 this.clearKeyPressBuffer();
6500 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6503 case OO
.ui
.Keys
.DOWN
:
6504 case OO
.ui
.Keys
.RIGHT
:
6505 this.clearKeyPressBuffer();
6506 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6509 case OO
.ui
.Keys
.ESCAPE
:
6510 case OO
.ui
.Keys
.TAB
:
6511 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6512 currentItem
.setHighlighted( false );
6514 this.unbindKeyDownListener();
6515 this.unbindKeyPressListener();
6516 // Don't prevent tabbing away / defocusing
6522 if ( nextItem
.constructor.static.highlightable
) {
6523 this.highlightItem( nextItem
);
6525 this.chooseItem( nextItem
);
6527 this.scrollItemIntoView( nextItem
);
6532 e
.stopPropagation();
6538 * Bind key down listener.
6542 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6543 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
6547 * Unbind key down listener.
6551 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6552 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
6556 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6558 * @param {OO.ui.OptionWidget} item Item to scroll into view
6560 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6562 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6563 // and around 100-150 ms after it is finished.
6564 this.blockMouseOverEvents
++;
6565 item
.scrollElementIntoView().done( function () {
6566 setTimeout( function () {
6567 widget
.blockMouseOverEvents
--;
6573 * Clear the key-press buffer
6577 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6578 if ( this.keyPressBufferTimer
) {
6579 clearTimeout( this.keyPressBufferTimer
);
6580 this.keyPressBufferTimer
= null;
6582 this.keyPressBuffer
= '';
6586 * Handle key press events.
6589 * @param {KeyboardEvent} e Key press event
6591 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
6592 var c
, filter
, item
;
6594 if ( !e
.charCode
) {
6595 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6596 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6601 if ( String
.fromCodePoint
) {
6602 c
= String
.fromCodePoint( e
.charCode
);
6604 c
= String
.fromCharCode( e
.charCode
);
6607 if ( this.keyPressBufferTimer
) {
6608 clearTimeout( this.keyPressBufferTimer
);
6610 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6612 item
= this.findHighlightedItem() || this.findSelectedItem();
6614 if ( this.keyPressBuffer
=== c
) {
6615 // Common (if weird) special case: typing "xxxx" will cycle through all
6616 // the items beginning with "x".
6618 item
= this.findRelativeSelectableItem( item
, 1 );
6621 this.keyPressBuffer
+= c
;
6624 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6625 if ( !item
|| !filter( item
) ) {
6626 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6629 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6630 this.highlightItem( item
);
6632 this.chooseItem( item
);
6634 this.scrollItemIntoView( item
);
6638 e
.stopPropagation();
6642 * Get a matcher for the specific string
6645 * @param {string} s String to match against items
6646 * @param {boolean} [exact=false] Only accept exact matches
6647 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6649 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6652 if ( s
.normalize
) {
6655 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6656 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6660 re
= new RegExp( re
, 'i' );
6661 return function ( item
) {
6662 var matchText
= item
.getMatchText();
6663 if ( matchText
.normalize
) {
6664 matchText
= matchText
.normalize();
6666 return re
.test( matchText
);
6671 * Bind key press listener.
6675 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6676 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
6680 * Unbind key down listener.
6682 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6687 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6688 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
6689 this.clearKeyPressBuffer();
6693 * Visibility change handler
6696 * @param {boolean} visible
6698 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6700 this.clearKeyPressBuffer();
6705 * Get the closest item to a jQuery.Event.
6708 * @param {jQuery.Event} e
6709 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6711 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6712 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6713 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6716 return $option
.data( 'oo-ui-optionWidget' ) || null;
6720 * Find selected item.
6722 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6724 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6727 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6728 if ( this.items
[ i
].isSelected() ) {
6729 return this.items
[ i
];
6736 * Get selected item.
6738 * @deprecated Since v0.25.0; use {@link #findSelectedItem} instead.
6739 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6741 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
6742 OO
.ui
.warnDeprecation( 'SelectWidget#getSelectedItem: Deprecated function. Use findSelectedItem instead. See T76630.' );
6743 return this.findSelectedItem();
6747 * Find highlighted item.
6749 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6751 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6754 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6755 if ( this.items
[ i
].isHighlighted() ) {
6756 return this.items
[ i
];
6763 * Toggle pressed state.
6765 * Press is a state that occurs when a user mouses down on an item, but
6766 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6767 * until the user releases the mouse.
6769 * @param {boolean} pressed An option is being pressed
6771 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6772 if ( pressed
=== undefined ) {
6773 pressed
= !this.pressed
;
6775 if ( pressed
!== this.pressed
) {
6777 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6778 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6779 this.pressed
= pressed
;
6784 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6785 * and any existing highlight will be removed. The highlight is mutually exclusive.
6787 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6791 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6792 var i
, len
, highlighted
,
6795 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6796 highlighted
= this.items
[ i
] === item
;
6797 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
6798 this.items
[ i
].setHighlighted( highlighted
);
6804 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6806 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6808 this.emit( 'highlight', item
);
6815 * Fetch an item by its label.
6817 * @param {string} label Label of the item to select.
6818 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6819 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6821 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
6823 len
= this.items
.length
,
6824 filter
= this.getItemMatcher( label
, true );
6826 for ( i
= 0; i
< len
; i
++ ) {
6827 item
= this.items
[ i
];
6828 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6835 filter
= this.getItemMatcher( label
, false );
6836 for ( i
= 0; i
< len
; i
++ ) {
6837 item
= this.items
[ i
];
6838 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6854 * Programmatically select an option by its label. If the item does not exist,
6855 * all options will be deselected.
6857 * @param {string} [label] Label of the item to select.
6858 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6862 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
6863 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
6864 if ( label
=== undefined || !itemFromLabel
) {
6865 return this.selectItem();
6867 return this.selectItem( itemFromLabel
);
6871 * Programmatically select an option by its data. If the `data` parameter is omitted,
6872 * or if the item does not exist, all options will be deselected.
6874 * @param {Object|string} [data] Value of the item to select, omit to deselect all
6878 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
6879 var itemFromData
= this.findItemFromData( data
);
6880 if ( data
=== undefined || !itemFromData
) {
6881 return this.selectItem();
6883 return this.selectItem( itemFromData
);
6887 * Programmatically select an option by its reference. If the `item` parameter is omitted,
6888 * all options will be deselected.
6890 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6894 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6895 var i
, len
, selected
,
6898 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6899 selected
= this.items
[ i
] === item
;
6900 if ( this.items
[ i
].isSelected() !== selected
) {
6901 this.items
[ i
].setSelected( selected
);
6906 if ( item
&& !item
.constructor.static.highlightable
) {
6908 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6910 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6913 this.emit( 'select', item
);
6922 * Press is a state that occurs when a user mouses down on an item, but has not
6923 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6924 * releases the mouse.
6926 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6930 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6931 var i
, len
, pressed
,
6934 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6935 pressed
= this.items
[ i
] === item
;
6936 if ( this.items
[ i
].isPressed() !== pressed
) {
6937 this.items
[ i
].setPressed( pressed
);
6942 this.emit( 'press', item
);
6951 * Note that ‘choose’ should never be modified programmatically. A user can choose
6952 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6953 * use the #selectItem method.
6955 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6956 * when users choose an item with the keyboard or mouse.
6958 * @param {OO.ui.OptionWidget} item Item to choose
6962 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6964 this.selectItem( item
);
6965 this.emit( 'choose', item
);
6972 * Find an option by its position relative to the specified item (or to the start of the option array,
6973 * if item is `null`). The direction in which to search through the option array is specified with a
6974 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6975 * `null` if there are no options in the array.
6977 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6978 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6979 * @param {Function} [filter] Only consider items for which this function returns
6980 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6981 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6983 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
6984 var currentIndex
, nextIndex
, i
,
6985 increase
= direction
> 0 ? 1 : -1,
6986 len
= this.items
.length
;
6988 if ( item
instanceof OO
.ui
.OptionWidget
) {
6989 currentIndex
= this.items
.indexOf( item
);
6990 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6992 // If no item is selected and moving forward, start at the beginning.
6993 // If moving backward, start at the end.
6994 nextIndex
= direction
> 0 ? 0 : len
- 1;
6997 for ( i
= 0; i
< len
; i
++ ) {
6998 item
= this.items
[ nextIndex
];
7000 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7001 ( !filter
|| filter( item
) )
7005 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7011 * Find the next selectable item or `null` if there are no selectable items.
7012 * Disabled options and menu-section markers and breaks are not selectable.
7014 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7016 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7017 return this.findRelativeSelectableItem( null, 1 );
7021 * Add an array of options to the select. Optionally, an index number can be used to
7022 * specify an insertion point.
7024 * @param {OO.ui.OptionWidget[]} items Items to add
7025 * @param {number} [index] Index to insert items after
7029 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7031 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7033 // Always provide an index, even if it was omitted
7034 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7040 * Remove the specified array of options from the select. Options will be detached
7041 * from the DOM, not removed, so they can be reused later. To remove all options from
7042 * the select, you may wish to use the #clearItems method instead.
7044 * @param {OO.ui.OptionWidget[]} items Items to remove
7048 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7051 // Deselect items being removed
7052 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7054 if ( item
.isSelected() ) {
7055 this.selectItem( null );
7060 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7062 this.emit( 'remove', items
);
7068 * Clear all options from the select. Options will be detached from the DOM, not removed,
7069 * so that they can be reused later. To remove a subset of options from the select, use
7070 * the #removeItems method.
7075 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7076 var items
= this.items
.slice();
7079 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7082 this.selectItem( null );
7084 this.emit( 'remove', items
);
7090 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7092 * Currently this is just used to set `aria-activedescendant` on it.
7095 * @param {jQuery} $focusOwner
7097 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7098 this.$focusOwner
= $focusOwner
;
7102 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7103 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7104 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7105 * options. For more information about options and selects, please see the
7106 * [OOUI documentation on MediaWiki][1].
7109 * // Decorated options in a select widget
7110 * var select = new OO.ui.SelectWidget( {
7112 * new OO.ui.DecoratedOptionWidget( {
7114 * label: 'Option with icon',
7117 * new OO.ui.DecoratedOptionWidget( {
7119 * label: 'Option with indicator',
7124 * $( 'body' ).append( select.$element );
7126 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7129 * @extends OO.ui.OptionWidget
7130 * @mixins OO.ui.mixin.IconElement
7131 * @mixins OO.ui.mixin.IndicatorElement
7134 * @param {Object} [config] Configuration options
7136 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7137 // Parent constructor
7138 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7140 // Mixin constructors
7141 OO
.ui
.mixin
.IconElement
.call( this, config
);
7142 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7146 .addClass( 'oo-ui-decoratedOptionWidget' )
7147 .prepend( this.$icon
)
7148 .append( this.$indicator
);
7153 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7154 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7155 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7158 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7159 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7160 * the [OOUI documentation on MediaWiki] [1] for more information.
7162 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7165 * @extends OO.ui.DecoratedOptionWidget
7168 * @param {Object} [config] Configuration options
7170 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7171 // Parent constructor
7172 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7175 this.$element
.addClass( 'oo-ui-menuOptionWidget' );
7180 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7182 /* Static Properties */
7188 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7191 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7192 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7195 * var myDropdown = new OO.ui.DropdownWidget( {
7198 * new OO.ui.MenuSectionOptionWidget( {
7201 * new OO.ui.MenuOptionWidget( {
7203 * label: 'Welsh Corgi'
7205 * new OO.ui.MenuOptionWidget( {
7207 * label: 'Standard Poodle'
7209 * new OO.ui.MenuSectionOptionWidget( {
7212 * new OO.ui.MenuOptionWidget( {
7219 * $( 'body' ).append( myDropdown.$element );
7222 * @extends OO.ui.DecoratedOptionWidget
7225 * @param {Object} [config] Configuration options
7227 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7228 // Parent constructor
7229 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7232 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7233 .removeAttr( 'role aria-selected' );
7238 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7240 /* Static Properties */
7246 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7252 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7255 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7256 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7257 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7258 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7259 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7260 * and customized to be opened, closed, and displayed as needed.
7262 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7263 * mouse outside the menu.
7265 * Menus also have support for keyboard interaction:
7267 * - Enter/Return key: choose and select a menu option
7268 * - Up-arrow key: highlight the previous menu option
7269 * - Down-arrow key: highlight the next menu option
7270 * - Esc key: hide the menu
7272 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7274 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7275 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7278 * @extends OO.ui.SelectWidget
7279 * @mixins OO.ui.mixin.ClippableElement
7280 * @mixins OO.ui.mixin.FloatableElement
7283 * @param {Object} [config] Configuration options
7284 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7285 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7286 * and {@link OO.ui.mixin.LookupElement LookupElement}
7287 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7288 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
7289 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7290 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7291 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7292 * that button, unless the button (or its parent widget) is passed in here.
7293 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7294 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7295 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7296 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7297 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7298 * @cfg {number} [width] Width of the menu
7300 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7301 // Configuration initialization
7302 config
= config
|| {};
7304 // Parent constructor
7305 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7307 // Mixin constructors
7308 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7309 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7312 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7313 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7314 this.filterFromInput
= !!config
.filterFromInput
;
7315 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7316 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7317 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7318 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7319 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7320 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7321 this.width
= config
.width
;
7324 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7325 if ( config
.widget
) {
7326 this.setFocusOwner( config
.widget
.$tabIndexed
);
7329 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7330 // that reference properties not initialized at that time of parent class construction
7331 // TODO: Find a better way to handle post-constructor setup
7332 this.visible
= false;
7333 this.$element
.addClass( 'oo-ui-element-hidden' );
7338 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7339 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7340 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7347 * The menu is ready: it is visible and has been positioned and clipped.
7353 * Handles document mouse down events.
7356 * @param {MouseEvent} e Mouse down event
7358 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7362 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7367 this.toggle( false );
7374 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
7375 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7377 if ( !this.isDisabled() && this.isVisible() ) {
7378 switch ( e
.keyCode
) {
7379 case OO
.ui
.Keys
.LEFT
:
7380 case OO
.ui
.Keys
.RIGHT
:
7381 // Do nothing if a text field is associated, arrow keys will be handled natively
7382 if ( !this.$input
) {
7383 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7386 case OO
.ui
.Keys
.ESCAPE
:
7387 case OO
.ui
.Keys
.TAB
:
7388 if ( currentItem
) {
7389 currentItem
.setHighlighted( false );
7391 this.toggle( false );
7392 // Don't prevent tabbing away, prevent defocusing
7393 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7395 e
.stopPropagation();
7399 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7406 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7407 * or after items were added/removed (always).
7411 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7412 var i
, item
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7413 firstItemFound
= false,
7415 len
= this.items
.length
,
7416 showAll
= !this.isVisible(),
7419 if ( this.$input
&& this.filterFromInput
) {
7420 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7421 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7423 // Hide non-matching options, and also hide section headers if all options
7424 // in their section are hidden.
7425 for ( i
= 0; i
< len
; i
++ ) {
7426 item
= this.items
[ i
];
7427 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7429 // If the previous section was empty, hide its header
7430 section
.toggle( showAll
|| !sectionEmpty
);
7433 sectionEmpty
= true;
7434 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7435 visible
= showAll
|| filter( item
);
7436 exactMatch
= exactMatch
|| exactFilter( item
);
7437 anyVisible
= anyVisible
|| visible
;
7438 sectionEmpty
= sectionEmpty
&& !visible
;
7439 item
.toggle( visible
);
7440 if ( this.highlightOnFilter
&& visible
&& !firstItemFound
) {
7441 // Highlight the first item in the list
7442 this.highlightItem( item
);
7443 firstItemFound
= true;
7447 // Process the final section
7449 section
.toggle( showAll
|| !sectionEmpty
);
7452 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7453 this.scrollItemIntoView( this.items
[ 0 ] );
7456 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7459 // Reevaluate clipping
7466 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
7467 if ( this.$input
) {
7468 this.$input
.on( 'keydown', this.onKeyDownHandler
);
7470 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
7477 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
7478 if ( this.$input
) {
7479 this.$input
.off( 'keydown', this.onKeyDownHandler
);
7481 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
7488 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
7489 if ( this.$input
) {
7490 if ( this.filterFromInput
) {
7491 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7492 this.updateItemVisibility();
7495 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
7502 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
7503 if ( this.$input
) {
7504 if ( this.filterFromInput
) {
7505 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7506 this.updateItemVisibility();
7509 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
7516 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7518 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7519 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7521 * @param {OO.ui.OptionWidget} item Item to choose
7524 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7525 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7526 if ( this.hideOnChoose
) {
7527 this.toggle( false );
7535 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7537 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7539 this.updateItemVisibility();
7547 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7549 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7551 this.updateItemVisibility();
7559 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7561 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7563 this.updateItemVisibility();
7569 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7570 * `.toggle( true )` after its #$element is attached to the DOM.
7572 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7573 * it in the right place and with the right dimensions only work correctly while it is attached.
7574 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7575 * strictly enforced, so currently it only generates a warning in the browser console.
7580 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7581 var change
, belowHeight
, aboveHeight
;
7583 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7584 change
= visible
!== this.isVisible();
7586 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7587 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7588 this.warnedUnattached
= true;
7592 if ( visible
&& ( this.width
|| this.$floatableContainer
) ) {
7593 this.setIdealSize( this.width
|| this.$floatableContainer
.width() );
7596 // Reset position before showing the popup again. It's possible we no longer need to flip
7597 // (e.g. if the user scrolled).
7598 this.setVerticalPosition( 'below' );
7603 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7607 this.bindKeyDownListener();
7608 this.bindKeyPressListener();
7610 this.togglePositioning( !!this.$floatableContainer
);
7611 this.toggleClipping( true );
7613 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7614 // If opening the menu downwards causes it to be clipped, flip it to open upwards instead
7615 belowHeight
= this.$element
.height();
7616 this.setVerticalPosition( 'above' );
7617 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7618 // If opening upwards also causes it to be clipped, flip it to open in whichever direction
7619 // we have more space
7620 aboveHeight
= this.$element
.height();
7621 if ( aboveHeight
< belowHeight
) {
7622 this.setVerticalPosition( 'below' );
7626 // Note that we do not flip the menu's opening direction if the clipping changes
7627 // later (e.g. after the user scrolls), that seems like it would be annoying
7629 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7631 if ( this.findSelectedItem() ) {
7632 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
7633 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
7637 if ( this.autoHide
) {
7638 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7641 this.emit( 'ready' );
7643 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7644 this.unbindKeyDownListener();
7645 this.unbindKeyPressListener();
7646 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7647 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7648 this.togglePositioning( false );
7649 this.toggleClipping( false );
7657 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7658 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7659 * users can interact with it.
7661 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7662 * OO.ui.DropdownInputWidget instead.
7665 * // Example: A DropdownWidget with a menu that contains three options
7666 * var dropDown = new OO.ui.DropdownWidget( {
7667 * label: 'Dropdown menu: Select a menu option',
7670 * new OO.ui.MenuOptionWidget( {
7674 * new OO.ui.MenuOptionWidget( {
7678 * new OO.ui.MenuOptionWidget( {
7686 * $( 'body' ).append( dropDown.$element );
7688 * dropDown.getMenu().selectItemByData( 'b' );
7690 * dropDown.getMenu().findSelectedItem().getData(); // returns 'b'
7692 * For more information, please see the [OOUI documentation on MediaWiki] [1].
7694 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7697 * @extends OO.ui.Widget
7698 * @mixins OO.ui.mixin.IconElement
7699 * @mixins OO.ui.mixin.IndicatorElement
7700 * @mixins OO.ui.mixin.LabelElement
7701 * @mixins OO.ui.mixin.TitledElement
7702 * @mixins OO.ui.mixin.TabIndexedElement
7705 * @param {Object} [config] Configuration options
7706 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7707 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7708 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7709 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7710 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
7712 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7713 // Configuration initialization
7714 config
= $.extend( { indicator
: 'down' }, config
);
7716 // Parent constructor
7717 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7719 // Properties (must be set before TabIndexedElement constructor call)
7720 this.$handle
= $( '<span>' );
7721 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
7723 // Mixin constructors
7724 OO
.ui
.mixin
.IconElement
.call( this, config
);
7725 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7726 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7727 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7728 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7731 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
7733 $floatableContainer
: this.$element
7738 click
: this.onClick
.bind( this ),
7739 keydown
: this.onKeyDown
.bind( this ),
7740 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7741 keypress
: this.menu
.onKeyPressHandler
,
7742 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
7744 this.menu
.connect( this, {
7745 select
: 'onMenuSelect',
7746 toggle
: 'onMenuToggle'
7751 .addClass( 'oo-ui-dropdownWidget-handle' )
7754 'aria-owns': this.menu
.getElementId(),
7755 'aria-autocomplete': 'list'
7757 .append( this.$icon
, this.$label
, this.$indicator
);
7759 .addClass( 'oo-ui-dropdownWidget' )
7760 .append( this.$handle
);
7761 this.$overlay
.append( this.menu
.$element
);
7766 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
7767 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
7768 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
7769 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
7770 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
7771 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7778 * @return {OO.ui.MenuSelectWidget} Menu of widget
7780 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
7785 * Handles menu select events.
7788 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7790 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
7794 this.setLabel( null );
7798 selectedLabel
= item
.getLabel();
7800 // If the label is a DOM element, clone it, because setLabel will append() it
7801 if ( selectedLabel
instanceof jQuery
) {
7802 selectedLabel
= selectedLabel
.clone();
7805 this.setLabel( selectedLabel
);
7809 * Handle menu toggle events.
7812 * @param {boolean} isVisible Open state of the menu
7814 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
7815 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
7818 this.$element
.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
7823 * Handle mouse click events.
7826 * @param {jQuery.Event} e Mouse click event
7828 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
7829 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7836 * Handle key down events.
7839 * @param {jQuery.Event} e Key down event
7841 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
7843 !this.isDisabled() &&
7845 e
.which
=== OO
.ui
.Keys
.ENTER
||
7847 e
.which
=== OO
.ui
.Keys
.SPACE
&&
7848 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
7849 // Space only closes the menu is the user is not typing to search.
7850 this.menu
.keyPressBuffer
=== ''
7853 !this.menu
.isVisible() &&
7855 e
.which
=== OO
.ui
.Keys
.UP
||
7856 e
.which
=== OO
.ui
.Keys
.DOWN
7867 * RadioOptionWidget is an option widget that looks like a radio button.
7868 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7869 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
7871 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
7874 * @extends OO.ui.OptionWidget
7877 * @param {Object} [config] Configuration options
7879 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7880 // Configuration initialization
7881 config
= config
|| {};
7883 // Properties (must be done before parent constructor which calls #setDisabled)
7884 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7886 // Parent constructor
7887 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7890 // Remove implicit role, we're handling it ourselves
7891 this.radio
.$input
.attr( 'role', 'presentation' );
7893 .addClass( 'oo-ui-radioOptionWidget' )
7894 .attr( 'role', 'radio' )
7895 .attr( 'aria-checked', 'false' )
7896 .removeAttr( 'aria-selected' )
7897 .prepend( this.radio
.$element
);
7902 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7904 /* Static Properties */
7910 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7916 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7922 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7928 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7935 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7936 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7938 this.radio
.setSelected( state
);
7940 .attr( 'aria-checked', state
.toString() )
7941 .removeAttr( 'aria-selected' );
7949 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7950 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7952 this.radio
.setDisabled( this.isDisabled() );
7958 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7959 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7960 * an interface for adding, removing and selecting options.
7961 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7963 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7964 * OO.ui.RadioSelectInputWidget instead.
7967 * // A RadioSelectWidget with RadioOptions.
7968 * var option1 = new OO.ui.RadioOptionWidget( {
7970 * label: 'Selected radio option'
7973 * var option2 = new OO.ui.RadioOptionWidget( {
7975 * label: 'Unselected radio option'
7978 * var radioSelect=new OO.ui.RadioSelectWidget( {
7979 * items: [ option1, option2 ]
7982 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7983 * radioSelect.selectItem( option1 );
7985 * $( 'body' ).append( radioSelect.$element );
7987 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7991 * @extends OO.ui.SelectWidget
7992 * @mixins OO.ui.mixin.TabIndexedElement
7995 * @param {Object} [config] Configuration options
7997 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
7998 // Parent constructor
7999 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8001 // Mixin constructors
8002 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8006 focus
: this.bindKeyDownListener
.bind( this ),
8007 blur
: this.unbindKeyDownListener
.bind( this )
8012 .addClass( 'oo-ui-radioSelectWidget' )
8013 .attr( 'role', 'radiogroup' );
8018 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8019 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8022 * MultioptionWidgets are special elements that can be selected and configured with data. The
8023 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8024 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8025 * and examples, please see the [OOUI documentation on MediaWiki][1].
8027 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8030 * @extends OO.ui.Widget
8031 * @mixins OO.ui.mixin.ItemWidget
8032 * @mixins OO.ui.mixin.LabelElement
8035 * @param {Object} [config] Configuration options
8036 * @cfg {boolean} [selected=false] Whether the option is initially selected
8038 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8039 // Configuration initialization
8040 config
= config
|| {};
8042 // Parent constructor
8043 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8045 // Mixin constructors
8046 OO
.ui
.mixin
.ItemWidget
.call( this );
8047 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8050 this.selected
= null;
8054 .addClass( 'oo-ui-multioptionWidget' )
8055 .append( this.$label
);
8056 this.setSelected( config
.selected
);
8061 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8062 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8063 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8070 * A change event is emitted when the selected state of the option changes.
8072 * @param {boolean} selected Whether the option is now selected
8078 * Check if the option is selected.
8080 * @return {boolean} Item is selected
8082 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8083 return this.selected
;
8087 * Set the option’s selected state. In general, all modifications to the selection
8088 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8089 * method instead of this method.
8091 * @param {boolean} [state=false] Select option
8094 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8096 if ( this.selected
!== state
) {
8097 this.selected
= state
;
8098 this.emit( 'change', state
);
8099 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8105 * MultiselectWidget allows selecting multiple options from a list.
8107 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8109 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8113 * @extends OO.ui.Widget
8114 * @mixins OO.ui.mixin.GroupWidget
8117 * @param {Object} [config] Configuration options
8118 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8120 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8121 // Parent constructor
8122 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8124 // Configuration initialization
8125 config
= config
|| {};
8127 // Mixin constructors
8128 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8131 this.aggregate( { change
: 'select' } );
8132 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
8133 // by GroupElement only when items are added/removed
8134 this.connect( this, { select
: [ 'emit', 'change' ] } );
8137 if ( config
.items
) {
8138 this.addItems( config
.items
);
8140 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8141 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8142 .append( this.$group
);
8147 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8148 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8155 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8161 * A select event is emitted when an item is selected or deselected.
8167 * Find options that are selected.
8169 * @return {OO.ui.MultioptionWidget[]} Selected options
8171 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8172 return this.items
.filter( function ( item
) {
8173 return item
.isSelected();
8178 * Get options that are selected.
8180 * @deprecated Since v0.25.0; use {@link #findSelectedItems} instead.
8181 * @return {OO.ui.MultioptionWidget[]} Selected options
8183 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
8184 OO
.ui
.warnDeprecation( 'MultiselectWidget#getSelectedItems: Deprecated function. Use findSelectedItems instead. See T76630.' );
8185 return this.findSelectedItems();
8189 * Find the data of options that are selected.
8191 * @return {Object[]|string[]} Values of selected options
8193 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8194 return this.findSelectedItems().map( function ( item
) {
8200 * Get the data of options that are selected.
8202 * @deprecated Since v0.25.0; use {@link #findSelectedItemsData} instead.
8203 * @return {Object[]|string[]} Values of selected options
8205 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
8206 OO
.ui
.warnDeprecation( 'MultiselectWidget#getSelectedItemsData: Deprecated function. Use findSelectedItemsData instead. See T76630.' );
8207 return this.findSelectedItemsData();
8211 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8213 * @param {OO.ui.MultioptionWidget[]} items Items to select
8216 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8217 this.items
.forEach( function ( item
) {
8218 var selected
= items
.indexOf( item
) !== -1;
8219 item
.setSelected( selected
);
8225 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8227 * @param {Object[]|string[]} datas Values of items to select
8230 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8233 items
= datas
.map( function ( data
) {
8234 return widget
.findItemFromData( data
);
8236 this.selectItems( items
);
8241 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8242 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8243 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8245 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8248 * @extends OO.ui.MultioptionWidget
8251 * @param {Object} [config] Configuration options
8253 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8254 // Configuration initialization
8255 config
= config
|| {};
8257 // Properties (must be done before parent constructor which calls #setDisabled)
8258 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8260 // Parent constructor
8261 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8264 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8265 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8269 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8270 .prepend( this.checkbox
.$element
);
8275 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8277 /* Static Properties */
8283 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8288 * Handle checkbox selected state change.
8292 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8293 this.setSelected( this.checkbox
.isSelected() );
8299 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8300 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8301 this.checkbox
.setSelected( state
);
8308 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8309 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8310 this.checkbox
.setDisabled( this.isDisabled() );
8317 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8318 this.checkbox
.focus();
8322 * Handle key down events.
8325 * @param {jQuery.Event} e
8327 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8329 element
= this.getElementGroup(),
8332 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8333 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8334 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8335 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8345 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8346 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8347 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8348 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8350 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8351 * OO.ui.CheckboxMultiselectInputWidget instead.
8354 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8355 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8358 * label: 'Selected checkbox'
8361 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8363 * label: 'Unselected checkbox'
8366 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8367 * items: [ option1, option2 ]
8370 * $( 'body' ).append( multiselect.$element );
8372 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8375 * @extends OO.ui.MultiselectWidget
8378 * @param {Object} [config] Configuration options
8380 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8381 // Parent constructor
8382 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8385 this.$lastClicked
= null;
8388 this.$group
.on( 'click', this.onClick
.bind( this ) );
8392 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8397 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8402 * Get an option by its position relative to the specified item (or to the start of the option array,
8403 * if item is `null`). The direction in which to search through the option array is specified with a
8404 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8405 * `null` if there are no options in the array.
8407 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8408 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8409 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8411 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8412 var currentIndex
, nextIndex
, i
,
8413 increase
= direction
> 0 ? 1 : -1,
8414 len
= this.items
.length
;
8417 currentIndex
= this.items
.indexOf( item
);
8418 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8420 // If no item is selected and moving forward, start at the beginning.
8421 // If moving backward, start at the end.
8422 nextIndex
= direction
> 0 ? 0 : len
- 1;
8425 for ( i
= 0; i
< len
; i
++ ) {
8426 item
= this.items
[ nextIndex
];
8427 if ( item
&& !item
.isDisabled() ) {
8430 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8436 * Handle click events on checkboxes.
8438 * @param {jQuery.Event} e
8440 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8441 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8442 $lastClicked
= this.$lastClicked
,
8443 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8444 .not( '.oo-ui-widget-disabled' );
8446 // Allow selecting multiple options at once by Shift-clicking them
8447 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8448 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8449 lastClickedIndex
= $options
.index( $lastClicked
);
8450 nowClickedIndex
= $options
.index( $nowClicked
);
8451 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8452 // browser. In either case we don't need custom handling.
8453 if ( nowClickedIndex
!== lastClickedIndex
) {
8455 wasSelected
= items
[ nowClickedIndex
].isSelected();
8456 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8458 // This depends on the DOM order of the items and the order of the .items array being the same.
8459 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8460 if ( !items
[ i
].isDisabled() ) {
8461 items
[ i
].setSelected( !wasSelected
);
8464 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8465 // handling first, then set our value. The order in which events happen is different for
8466 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8467 // non-click actions that change the checkboxes.
8469 setTimeout( function () {
8470 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8471 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8477 if ( $nowClicked
.length
) {
8478 this.$lastClicked
= $nowClicked
;
8487 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8489 if ( !this.isDisabled() ) {
8490 item
= this.getRelativeFocusableItem( null, 1 );
8501 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8506 * Progress bars visually display the status of an operation, such as a download,
8507 * and can be either determinate or indeterminate:
8509 * - **determinate** process bars show the percent of an operation that is complete.
8511 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8512 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8513 * not use percentages.
8515 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8518 * // Examples of determinate and indeterminate progress bars.
8519 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8522 * var progressBar2 = new OO.ui.ProgressBarWidget();
8524 * // Create a FieldsetLayout to layout progress bars
8525 * var fieldset = new OO.ui.FieldsetLayout;
8526 * fieldset.addItems( [
8527 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8528 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8530 * $( 'body' ).append( fieldset.$element );
8533 * @extends OO.ui.Widget
8536 * @param {Object} [config] Configuration options
8537 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8538 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8539 * By default, the progress bar is indeterminate.
8541 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8542 // Configuration initialization
8543 config
= config
|| {};
8545 // Parent constructor
8546 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8549 this.$bar
= $( '<div>' );
8550 this.progress
= null;
8553 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8554 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8557 role
: 'progressbar',
8559 'aria-valuemax': 100
8561 .addClass( 'oo-ui-progressBarWidget' )
8562 .append( this.$bar
);
8567 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8569 /* Static Properties */
8575 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8580 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8582 * @return {number|boolean} Progress percent
8584 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8585 return this.progress
;
8589 * Set the percent of the process completed or `false` for an indeterminate process.
8591 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8593 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8594 this.progress
= progress
;
8596 if ( progress
!== false ) {
8597 this.$bar
.css( 'width', this.progress
+ '%' );
8598 this.$element
.attr( 'aria-valuenow', this.progress
);
8600 this.$bar
.css( 'width', '' );
8601 this.$element
.removeAttr( 'aria-valuenow' );
8603 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8607 * InputWidget is the base class for all input widgets, which
8608 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8609 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8610 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8612 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8616 * @extends OO.ui.Widget
8617 * @mixins OO.ui.mixin.FlaggedElement
8618 * @mixins OO.ui.mixin.TabIndexedElement
8619 * @mixins OO.ui.mixin.TitledElement
8620 * @mixins OO.ui.mixin.AccessKeyedElement
8623 * @param {Object} [config] Configuration options
8624 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8625 * @cfg {string} [value=''] The value of the input.
8626 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8627 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8628 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8629 * before it is accepted.
8631 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8632 // Configuration initialization
8633 config
= config
|| {};
8635 // Parent constructor
8636 OO
.ui
.InputWidget
.parent
.call( this, config
);
8639 // See #reusePreInfuseDOM about config.$input
8640 this.$input
= config
.$input
|| this.getInputElement( config
);
8642 this.inputFilter
= config
.inputFilter
;
8644 // Mixin constructors
8645 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8646 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8647 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8648 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8651 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8655 .addClass( 'oo-ui-inputWidget-input' )
8656 .attr( 'name', config
.name
)
8657 .prop( 'disabled', this.isDisabled() );
8659 .addClass( 'oo-ui-inputWidget' )
8660 .append( this.$input
);
8661 this.setValue( config
.value
);
8663 this.setDir( config
.dir
);
8665 if ( config
.inputId
!== undefined ) {
8666 this.setInputId( config
.inputId
);
8672 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8673 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8674 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8675 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8676 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8678 /* Static Methods */
8683 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8684 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8685 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8686 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8693 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8694 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8695 if ( config
.$input
&& config
.$input
.length
) {
8696 state
.value
= config
.$input
.val();
8697 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8698 state
.focus
= config
.$input
.is( ':focus' );
8708 * A change event is emitted when the value of the input changes.
8710 * @param {string} value
8716 * Get input element.
8718 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8719 * different circumstances. The element must have a `value` property (like form elements).
8722 * @param {Object} config Configuration options
8723 * @return {jQuery} Input element
8725 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8726 return $( '<input>' );
8730 * Handle potentially value-changing events.
8733 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8735 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8737 if ( !this.isDisabled() ) {
8738 // Allow the stack to clear so the value will be updated
8739 setTimeout( function () {
8740 widget
.setValue( widget
.$input
.val() );
8746 * Get the value of the input.
8748 * @return {string} Input value
8750 OO
.ui
.InputWidget
.prototype.getValue = function () {
8751 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8752 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8753 var value
= this.$input
.val();
8754 if ( this.value
!== value
) {
8755 this.setValue( value
);
8761 * Set the directionality of the input.
8763 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8766 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8767 this.$input
.prop( 'dir', dir
);
8772 * Set the value of the input.
8774 * @param {string} value New value
8778 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8779 value
= this.cleanUpValue( value
);
8780 // Update the DOM if it has changed. Note that with cleanUpValue, it
8781 // is possible for the DOM value to change without this.value changing.
8782 if ( this.$input
.val() !== value
) {
8783 this.$input
.val( value
);
8785 if ( this.value
!== value
) {
8787 this.emit( 'change', this.value
);
8789 // The first time that the value is set (probably while constructing the widget),
8790 // remember it in defaultValue. This property can be later used to check whether
8791 // the value of the input has been changed since it was created.
8792 if ( this.defaultValue
=== undefined ) {
8793 this.defaultValue
= this.value
;
8794 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
8800 * Clean up incoming value.
8802 * Ensures value is a string, and converts undefined and null to empty string.
8805 * @param {string} value Original value
8806 * @return {string} Cleaned up value
8808 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8809 if ( value
=== undefined || value
=== null ) {
8811 } else if ( this.inputFilter
) {
8812 return this.inputFilter( String( value
) );
8814 return String( value
);
8821 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8822 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8823 if ( this.$input
) {
8824 this.$input
.prop( 'disabled', this.isDisabled() );
8830 * Set the 'id' attribute of the `<input>` element.
8832 * @param {string} id
8835 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
8836 this.$input
.attr( 'id', id
);
8843 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8844 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8845 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8846 this.setValue( state
.value
);
8848 if ( state
.focus
) {
8854 * Data widget intended for creating 'hidden'-type inputs.
8857 * @extends OO.ui.Widget
8860 * @param {Object} [config] Configuration options
8861 * @cfg {string} [value=''] The value of the input.
8862 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8864 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
8865 // Configuration initialization
8866 config
= $.extend( { value
: '', name
: '' }, config
);
8868 // Parent constructor
8869 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
8872 this.$element
.attr( {
8874 value
: config
.value
,
8877 this.$element
.removeAttr( 'aria-disabled' );
8882 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
8884 /* Static Properties */
8890 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
8893 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8894 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8895 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8896 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8897 * [OOUI documentation on MediaWiki] [1] for more information.
8900 * // A ButtonInputWidget rendered as an HTML button, the default.
8901 * var button = new OO.ui.ButtonInputWidget( {
8902 * label: 'Input button',
8906 * $( 'body' ).append( button.$element );
8908 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
8911 * @extends OO.ui.InputWidget
8912 * @mixins OO.ui.mixin.ButtonElement
8913 * @mixins OO.ui.mixin.IconElement
8914 * @mixins OO.ui.mixin.IndicatorElement
8915 * @mixins OO.ui.mixin.LabelElement
8916 * @mixins OO.ui.mixin.TitledElement
8919 * @param {Object} [config] Configuration options
8920 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8921 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8922 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8923 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8924 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8926 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8927 // Configuration initialization
8928 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8930 // See InputWidget#reusePreInfuseDOM about config.$input
8931 if ( config
.$input
) {
8932 config
.$input
.empty();
8935 // Properties (must be set before parent constructor, which calls #setValue)
8936 this.useInputTag
= config
.useInputTag
;
8938 // Parent constructor
8939 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8941 // Mixin constructors
8942 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8943 OO
.ui
.mixin
.IconElement
.call( this, config
);
8944 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8945 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8946 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8949 if ( !config
.useInputTag
) {
8950 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8952 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8957 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8958 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8959 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8960 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8961 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8962 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8964 /* Static Properties */
8970 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8978 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8980 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8981 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8987 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8989 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8990 * text, or `null` for no label
8993 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8994 if ( typeof label
=== 'function' ) {
8995 label
= OO
.ui
.resolveMsg( label
);
8998 if ( this.useInputTag
) {
8999 // Discard non-plaintext labels
9000 if ( typeof label
!== 'string' ) {
9004 this.$input
.val( label
);
9007 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9011 * Set the value of the input.
9013 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9014 * they do not support {@link #value values}.
9016 * @param {string} value New value
9019 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9020 if ( !this.useInputTag
) {
9021 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9029 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9030 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9031 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9036 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9037 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9038 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9039 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9041 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9044 * // An example of selected, unselected, and disabled checkbox inputs
9045 * var checkbox1=new OO.ui.CheckboxInputWidget( {
9049 * var checkbox2=new OO.ui.CheckboxInputWidget( {
9052 * var checkbox3=new OO.ui.CheckboxInputWidget( {
9056 * // Create a fieldset layout with fields for each checkbox.
9057 * var fieldset = new OO.ui.FieldsetLayout( {
9058 * label: 'Checkboxes'
9060 * fieldset.addItems( [
9061 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9062 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9063 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9065 * $( 'body' ).append( fieldset.$element );
9067 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9070 * @extends OO.ui.InputWidget
9073 * @param {Object} [config] Configuration options
9074 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9076 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9077 // Configuration initialization
9078 config
= config
|| {};
9080 // Parent constructor
9081 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9085 .addClass( 'oo-ui-checkboxInputWidget' )
9086 // Required for pretty styling in WikimediaUI theme
9087 .append( $( '<span>' ) );
9088 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9093 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9095 /* Static Properties */
9101 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9103 /* Static Methods */
9108 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9109 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9110 state
.checked
= config
.$input
.prop( 'checked' );
9120 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9121 return $( '<input>' ).attr( 'type', 'checkbox' );
9127 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9129 if ( !this.isDisabled() ) {
9130 // Allow the stack to clear so the value will be updated
9131 setTimeout( function () {
9132 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9138 * Set selection state of this checkbox.
9140 * @param {boolean} state `true` for selected
9143 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9145 if ( this.selected
!== state
) {
9146 this.selected
= state
;
9147 this.$input
.prop( 'checked', this.selected
);
9148 this.emit( 'change', this.selected
);
9150 // The first time that the selection state is set (probably while constructing the widget),
9151 // remember it in defaultSelected. This property can be later used to check whether
9152 // the selection state of the input has been changed since it was created.
9153 if ( this.defaultSelected
=== undefined ) {
9154 this.defaultSelected
= this.selected
;
9155 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9161 * Check if this checkbox is selected.
9163 * @return {boolean} Checkbox is selected
9165 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9166 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9167 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9168 var selected
= this.$input
.prop( 'checked' );
9169 if ( this.selected
!== selected
) {
9170 this.setSelected( selected
);
9172 return this.selected
;
9178 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9179 if ( !this.isDisabled() ) {
9180 this.$input
.click();
9188 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9189 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9190 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9191 this.setSelected( state
.checked
);
9196 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9197 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9198 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9199 * more information about input widgets.
9201 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9202 * are no options. If no `value` configuration option is provided, the first option is selected.
9203 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9205 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9208 * // Example: A DropdownInputWidget with three options
9209 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9211 * { data: 'a', label: 'First' },
9212 * { data: 'b', label: 'Second'},
9213 * { data: 'c', label: 'Third' }
9216 * $( 'body' ).append( dropdownInput.$element );
9218 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9221 * @extends OO.ui.InputWidget
9224 * @param {Object} [config] Configuration options
9225 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9226 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9228 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9229 // Configuration initialization
9230 config
= config
|| {};
9232 // Properties (must be done before parent constructor which calls #setDisabled)
9233 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
9234 // Set up the options before parent constructor, which uses them to validate config.value.
9235 // Use this instead of setOptions() because this.$input is not set up yet.
9236 this.setOptionsData( config
.options
|| [] );
9238 // Parent constructor
9239 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9242 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9246 .addClass( 'oo-ui-dropdownInputWidget' )
9247 .append( this.dropdownWidget
.$element
);
9248 this.setTabIndexedElement( null );
9253 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9261 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9262 return $( '<select>' );
9266 * Handles menu select events.
9269 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9271 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9272 this.setValue( item
? item
.getData() : '' );
9278 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9280 value
= this.cleanUpValue( value
);
9281 // Only allow setting values that are actually present in the dropdown
9282 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9283 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9284 this.dropdownWidget
.getMenu().selectItem( selected
);
9285 value
= selected
? selected
.getData() : '';
9286 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9287 if ( this.optionsDirty
) {
9288 // We reached this from the constructor or from #setOptions.
9289 // We have to update the <select> element.
9290 this.updateOptionsInterface();
9298 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9299 this.dropdownWidget
.setDisabled( state
);
9300 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9305 * Set the options available for this input.
9307 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9310 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9311 var value
= this.getValue();
9313 this.setOptionsData( options
);
9315 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9316 // In case the previous value is no longer an available option, select the first valid one.
9317 this.setValue( value
);
9323 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9325 * This method may be called before the parent constructor, so various properties may not be
9328 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9331 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9336 this.optionsDirty
= true;
9338 optionWidgets
= options
.map( function ( opt
) {
9341 if ( opt
.optgroup
!== undefined ) {
9342 return widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9345 optValue
= widget
.cleanUpValue( opt
.data
);
9346 return widget
.createMenuOptionWidget(
9348 opt
.label
!== undefined ? opt
.label
: optValue
9353 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9357 * Create a menu option widget.
9360 * @param {string} data Item data
9361 * @param {string} label Item label
9362 * @return {OO.ui.MenuOptionWidget} Option widget
9364 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9365 return new OO
.ui
.MenuOptionWidget( {
9372 * Create a menu section option widget.
9375 * @param {string} label Section item label
9376 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9378 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9379 return new OO
.ui
.MenuSectionOptionWidget( {
9385 * Update the user-visible interface to match the internal list of options and value.
9387 * This method must only be called after the parent constructor.
9391 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9393 $optionsContainer
= this.$input
,
9394 defaultValue
= this.defaultValue
,
9397 this.$input
.empty();
9399 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9402 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9403 $optionNode
= $( '<option>' )
9404 .attr( 'value', optionWidget
.getData() )
9405 .text( optionWidget
.getLabel() );
9407 // Remember original selection state. This property can be later used to check whether
9408 // the selection state of the input has been changed since it was created.
9409 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9411 $optionsContainer
.append( $optionNode
);
9413 $optionNode
= $( '<optgroup>' )
9414 .attr( 'label', optionWidget
.getLabel() );
9415 widget
.$input
.append( $optionNode
);
9416 $optionsContainer
= $optionNode
;
9420 this.optionsDirty
= false;
9426 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9427 this.dropdownWidget
.focus();
9434 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9435 this.dropdownWidget
.blur();
9440 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9441 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9442 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9443 * please see the [OOUI documentation on MediaWiki][1].
9445 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9448 * // An example of selected, unselected, and disabled radio inputs
9449 * var radio1 = new OO.ui.RadioInputWidget( {
9453 * var radio2 = new OO.ui.RadioInputWidget( {
9456 * var radio3 = new OO.ui.RadioInputWidget( {
9460 * // Create a fieldset layout with fields for each radio button.
9461 * var fieldset = new OO.ui.FieldsetLayout( {
9462 * label: 'Radio inputs'
9464 * fieldset.addItems( [
9465 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9466 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9467 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9469 * $( 'body' ).append( fieldset.$element );
9471 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9474 * @extends OO.ui.InputWidget
9477 * @param {Object} [config] Configuration options
9478 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9480 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9481 // Configuration initialization
9482 config
= config
|| {};
9484 // Parent constructor
9485 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9489 .addClass( 'oo-ui-radioInputWidget' )
9490 // Required for pretty styling in WikimediaUI theme
9491 .append( $( '<span>' ) );
9492 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9497 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9499 /* Static Properties */
9505 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9507 /* Static Methods */
9512 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9513 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9514 state
.checked
= config
.$input
.prop( 'checked' );
9524 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9525 return $( '<input>' ).attr( 'type', 'radio' );
9531 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9532 // RadioInputWidget doesn't track its state.
9536 * Set selection state of this radio button.
9538 * @param {boolean} state `true` for selected
9541 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9542 // RadioInputWidget doesn't track its state.
9543 this.$input
.prop( 'checked', state
);
9544 // The first time that the selection state is set (probably while constructing the widget),
9545 // remember it in defaultSelected. This property can be later used to check whether
9546 // the selection state of the input has been changed since it was created.
9547 if ( this.defaultSelected
=== undefined ) {
9548 this.defaultSelected
= state
;
9549 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9555 * Check if this radio button is selected.
9557 * @return {boolean} Radio is selected
9559 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9560 return this.$input
.prop( 'checked' );
9566 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9567 if ( !this.isDisabled() ) {
9568 this.$input
.click();
9576 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9577 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9578 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9579 this.setSelected( state
.checked
);
9584 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9585 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9586 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9587 * more information about input widgets.
9589 * This and OO.ui.DropdownInputWidget support the same configuration options.
9592 * // Example: A RadioSelectInputWidget with three options
9593 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9595 * { data: 'a', label: 'First' },
9596 * { data: 'b', label: 'Second'},
9597 * { data: 'c', label: 'Third' }
9600 * $( 'body' ).append( radioSelectInput.$element );
9602 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9605 * @extends OO.ui.InputWidget
9608 * @param {Object} [config] Configuration options
9609 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9611 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9612 // Configuration initialization
9613 config
= config
|| {};
9615 // Properties (must be done before parent constructor which calls #setDisabled)
9616 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9617 // Set up the options before parent constructor, which uses them to validate config.value.
9618 // Use this instead of setOptions() because this.$input is not set up yet
9619 this.setOptionsData( config
.options
|| [] );
9621 // Parent constructor
9622 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9625 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9629 .addClass( 'oo-ui-radioSelectInputWidget' )
9630 .append( this.radioSelectWidget
.$element
);
9631 this.setTabIndexedElement( null );
9636 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9638 /* Static Methods */
9643 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9644 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9645 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9652 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9653 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9654 // Cannot reuse the `<input type=radio>` set
9655 delete config
.$input
;
9665 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9666 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
9667 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
9668 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
9672 * Handles menu select events.
9675 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9677 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9678 this.setValue( item
.getData() );
9684 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9686 value
= this.cleanUpValue( value
);
9687 // Only allow setting values that are actually present in the dropdown
9688 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
9689 this.radioSelectWidget
.findFirstSelectableItem();
9690 this.radioSelectWidget
.selectItem( selected
);
9691 value
= selected
? selected
.getData() : '';
9692 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9699 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9700 this.radioSelectWidget
.setDisabled( state
);
9701 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9706 * Set the options available for this input.
9708 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9711 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9712 var value
= this.getValue();
9714 this.setOptionsData( options
);
9716 // Re-set the value to update the visible interface (RadioSelectWidget).
9717 // In case the previous value is no longer an available option, select the first valid one.
9718 this.setValue( value
);
9724 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9726 * This method may be called before the parent constructor, so various properties may not be
9729 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9732 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
9735 this.radioSelectWidget
9737 .addItems( options
.map( function ( opt
) {
9738 var optValue
= widget
.cleanUpValue( opt
.data
);
9739 return new OO
.ui
.RadioOptionWidget( {
9741 label
: opt
.label
!== undefined ? opt
.label
: optValue
9749 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
9750 this.radioSelectWidget
.focus();
9757 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
9758 this.radioSelectWidget
.blur();
9763 * CheckboxMultiselectInputWidget is a
9764 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9765 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9766 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
9767 * more information about input widgets.
9770 * // Example: A CheckboxMultiselectInputWidget with three options
9771 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9773 * { data: 'a', label: 'First' },
9774 * { data: 'b', label: 'Second'},
9775 * { data: 'c', label: 'Third' }
9778 * $( 'body' ).append( multiselectInput.$element );
9780 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9783 * @extends OO.ui.InputWidget
9786 * @param {Object} [config] Configuration options
9787 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9789 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9790 // Configuration initialization
9791 config
= config
|| {};
9793 // Properties (must be done before parent constructor which calls #setDisabled)
9794 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9795 // Must be set before the #setOptionsData call below
9796 this.inputName
= config
.name
;
9797 // Set up the options before parent constructor, which uses them to validate config.value.
9798 // Use this instead of setOptions() because this.$input is not set up yet
9799 this.setOptionsData( config
.options
|| [] );
9801 // Parent constructor
9802 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9805 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
9809 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9810 .append( this.checkboxMultiselectWidget
.$element
);
9811 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9812 this.$input
.detach();
9817 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9819 /* Static Methods */
9824 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9825 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9826 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9827 .toArray().map( function ( el
) { return el
.value
; } );
9834 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9835 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9836 // Cannot reuse the `<input type=checkbox>` set
9837 delete config
.$input
;
9847 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9849 return $( '<unused>' );
9853 * Handles CheckboxMultiselectWidget select events.
9857 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
9858 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
9864 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9865 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9866 .toArray().map( function ( el
) { return el
.value
; } );
9867 if ( this.value
!== value
) {
9868 this.setValue( value
);
9876 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9877 value
= this.cleanUpValue( value
);
9878 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9879 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9880 if ( this.optionsDirty
) {
9881 // We reached this from the constructor or from #setOptions.
9882 // We have to update the <select> element.
9883 this.updateOptionsInterface();
9889 * Clean up incoming value.
9891 * @param {string[]} value Original value
9892 * @return {string[]} Cleaned up value
9894 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9897 if ( !Array
.isArray( value
) ) {
9900 for ( i
= 0; i
< value
.length
; i
++ ) {
9902 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9903 // Remove options that we don't have here
9904 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
9907 cleanValue
.push( singleValue
);
9915 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9916 this.checkboxMultiselectWidget
.setDisabled( state
);
9917 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9922 * Set the options available for this input.
9924 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9927 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9928 var value
= this.getValue();
9930 this.setOptionsData( options
);
9932 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
9933 // This will also get rid of any stale options that we just removed.
9934 this.setValue( value
);
9940 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9942 * This method may be called before the parent constructor, so various properties may not be
9945 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9948 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
9951 this.optionsDirty
= true;
9953 this.checkboxMultiselectWidget
9955 .addItems( options
.map( function ( opt
) {
9956 var optValue
, item
, optDisabled
;
9958 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9959 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9960 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9962 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9963 disabled
: optDisabled
9965 // Set the 'name' and 'value' for form submission
9966 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9967 item
.checkbox
.setValue( optValue
);
9973 * Update the user-visible interface to match the internal list of options and value.
9975 * This method must only be called after the parent constructor.
9979 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
9980 var defaultValue
= this.defaultValue
;
9982 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
9983 // Remember original selection state. This property can be later used to check whether
9984 // the selection state of the input has been changed since it was created.
9985 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
9986 item
.checkbox
.defaultSelected
= isDefault
;
9987 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
9990 this.optionsDirty
= false;
9996 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
9997 this.checkboxMultiselectWidget
.focus();
10002 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10003 * size of the field as well as its presentation. In addition, these widgets can be configured
10004 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
10005 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10006 * which modifies incoming values rather than validating them.
10007 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10009 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10012 * // Example of a text input widget
10013 * var textInput = new OO.ui.TextInputWidget( {
10014 * value: 'Text input'
10016 * $( 'body' ).append( textInput.$element );
10018 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10021 * @extends OO.ui.InputWidget
10022 * @mixins OO.ui.mixin.IconElement
10023 * @mixins OO.ui.mixin.IndicatorElement
10024 * @mixins OO.ui.mixin.PendingElement
10025 * @mixins OO.ui.mixin.LabelElement
10028 * @param {Object} [config] Configuration options
10029 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10030 * 'email', 'url' or 'number'.
10031 * @cfg {string} [placeholder] Placeholder text
10032 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10033 * instruct the browser to focus this widget.
10034 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10035 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10037 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10038 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10039 * many emojis) count as 2 characters each.
10040 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10041 * the value or placeholder text: `'before'` or `'after'`
10042 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10043 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10044 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10045 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10046 * leaving it up to the browser).
10047 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10048 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10049 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10050 * value for it to be considered valid; when Function, a function receiving the value as parameter
10051 * that must return true, or promise resolving to true, for it to be considered valid.
10053 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10054 // Configuration initialization
10055 config
= $.extend( {
10057 labelPosition
: 'after'
10060 if ( config
.multiline
) {
10061 OO
.ui
.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
10062 return new OO
.ui
.MultilineTextInputWidget( config
);
10065 // Parent constructor
10066 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10068 // Mixin constructors
10069 OO
.ui
.mixin
.IconElement
.call( this, config
);
10070 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10071 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10072 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10075 this.type
= this.getSaneType( config
);
10076 this.readOnly
= false;
10077 this.required
= false;
10078 this.validate
= null;
10079 this.styleHeight
= null;
10080 this.scrollWidth
= null;
10082 this.setValidation( config
.validate
);
10083 this.setLabelPosition( config
.labelPosition
);
10087 keypress
: this.onKeyPress
.bind( this ),
10088 blur
: this.onBlur
.bind( this ),
10089 focus
: this.onFocus
.bind( this )
10091 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10092 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10093 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10094 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10098 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10099 .append( this.$icon
, this.$indicator
);
10100 this.setReadOnly( !!config
.readOnly
);
10101 this.setRequired( !!config
.required
);
10102 if ( config
.placeholder
!== undefined ) {
10103 this.$input
.attr( 'placeholder', config
.placeholder
);
10105 if ( config
.maxLength
!== undefined ) {
10106 this.$input
.attr( 'maxlength', config
.maxLength
);
10108 if ( config
.autofocus
) {
10109 this.$input
.attr( 'autofocus', 'autofocus' );
10111 if ( config
.autocomplete
=== false ) {
10112 this.$input
.attr( 'autocomplete', 'off' );
10113 // Turning off autocompletion also disables "form caching" when the user navigates to a
10114 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10116 beforeunload: function () {
10117 this.$input
.removeAttr( 'autocomplete' );
10119 pageshow: function () {
10120 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10121 // whole page... it shouldn't hurt, though.
10122 this.$input
.attr( 'autocomplete', 'off' );
10126 if ( config
.spellcheck
!== undefined ) {
10127 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10129 if ( this.label
) {
10130 this.isWaitingToBeAttached
= true;
10131 this.installParentChangeDetector();
10137 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10138 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10139 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10140 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10141 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10143 /* Static Properties */
10145 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10153 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10161 * Handle icon mouse down events.
10164 * @param {jQuery.Event} e Mouse down event
10166 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10167 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10174 * Handle indicator mouse down events.
10177 * @param {jQuery.Event} e Mouse down event
10179 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10180 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10187 * Handle key press events.
10190 * @param {jQuery.Event} e Key press event
10191 * @fires enter If enter key is pressed
10193 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10194 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10195 this.emit( 'enter', e
);
10200 * Handle blur events.
10203 * @param {jQuery.Event} e Blur event
10205 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10206 this.setValidityFlag();
10210 * Handle focus events.
10213 * @param {jQuery.Event} e Focus event
10215 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10216 if ( this.isWaitingToBeAttached
) {
10217 // If we've received focus, then we must be attached to the document, and if
10218 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10219 this.onElementAttach();
10221 this.setValidityFlag( true );
10225 * Handle element attach events.
10228 * @param {jQuery.Event} e Element attach event
10230 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10231 this.isWaitingToBeAttached
= false;
10232 // Any previously calculated size is now probably invalid if we reattached elsewhere
10233 this.valCache
= null;
10234 this.positionLabel();
10238 * Handle debounced change events.
10240 * @param {string} value
10243 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10244 this.setValidityFlag();
10248 * Check if the input is {@link #readOnly read-only}.
10250 * @return {boolean}
10252 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10253 return this.readOnly
;
10257 * Set the {@link #readOnly read-only} state of the input.
10259 * @param {boolean} state Make input read-only
10262 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10263 this.readOnly
= !!state
;
10264 this.$input
.prop( 'readOnly', this.readOnly
);
10269 * Check if the input is {@link #required required}.
10271 * @return {boolean}
10273 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10274 return this.required
;
10278 * Set the {@link #required required} state of the input.
10280 * @param {boolean} state Make input required
10283 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10284 this.required
= !!state
;
10285 if ( this.required
) {
10287 .prop( 'required', true )
10288 .attr( 'aria-required', 'true' );
10289 if ( this.getIndicator() === null ) {
10290 this.setIndicator( 'required' );
10294 .prop( 'required', false )
10295 .removeAttr( 'aria-required' );
10296 if ( this.getIndicator() === 'required' ) {
10297 this.setIndicator( null );
10304 * Support function for making #onElementAttach work across browsers.
10306 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10307 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10309 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10310 * first time that the element gets attached to the documented.
10312 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10313 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10314 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10317 if ( MutationObserver
) {
10318 // The new way. If only it wasn't so ugly.
10320 if ( this.isElementAttached() ) {
10321 // Widget is attached already, do nothing. This breaks the functionality of this function when
10322 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10323 // would require observation of the whole document, which would hurt performance of other,
10324 // more important code.
10328 // Find topmost node in the tree
10329 topmostNode
= this.$element
[ 0 ];
10330 while ( topmostNode
.parentNode
) {
10331 topmostNode
= topmostNode
.parentNode
;
10334 // We have no way to detect the $element being attached somewhere without observing the entire
10335 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10336 // parent node of $element, and instead detect when $element is removed from it (and thus
10337 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10338 // doesn't get attached, we end up back here and create the parent.
10340 mutationObserver
= new MutationObserver( function ( mutations
) {
10341 var i
, j
, removedNodes
;
10342 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10343 removedNodes
= mutations
[ i
].removedNodes
;
10344 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10345 if ( removedNodes
[ j
] === topmostNode
) {
10346 setTimeout( onRemove
, 0 );
10353 onRemove = function () {
10354 // If the node was attached somewhere else, report it
10355 if ( widget
.isElementAttached() ) {
10356 widget
.onElementAttach();
10358 mutationObserver
.disconnect();
10359 widget
.installParentChangeDetector();
10362 // Create a fake parent and observe it
10363 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10364 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10366 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10367 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10368 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10376 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10377 if ( this.getSaneType( config
) === 'number' ) {
10378 return $( '<input>' )
10379 .attr( 'step', 'any' )
10380 .attr( 'type', 'number' );
10382 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10387 * Get sanitized value for 'type' for given config.
10389 * @param {Object} config Configuration options
10390 * @return {string|null}
10393 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10394 var allowedTypes
= [
10401 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10405 * Focus the input and select a specified range within the text.
10407 * @param {number} from Select from offset
10408 * @param {number} [to] Select to offset, defaults to from
10411 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10412 var isBackwards
, start
, end
,
10413 input
= this.$input
[ 0 ];
10417 isBackwards
= to
< from;
10418 start
= isBackwards
? to
: from;
10419 end
= isBackwards
? from : to
;
10424 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10426 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10427 // Rather than expensively check if the input is attached every time, just check
10428 // if it was the cause of an error being thrown. If not, rethrow the error.
10429 if ( this.getElementDocument().body
.contains( input
) ) {
10437 * Get an object describing the current selection range in a directional manner
10439 * @return {Object} Object containing 'from' and 'to' offsets
10441 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10442 var input
= this.$input
[ 0 ],
10443 start
= input
.selectionStart
,
10444 end
= input
.selectionEnd
,
10445 isBackwards
= input
.selectionDirection
=== 'backward';
10448 from: isBackwards
? end
: start
,
10449 to
: isBackwards
? start
: end
10454 * Get the length of the text input value.
10456 * This could differ from the length of #getValue if the
10457 * value gets filtered
10459 * @return {number} Input length
10461 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10462 return this.$input
[ 0 ].value
.length
;
10466 * Focus the input and select the entire text.
10470 OO
.ui
.TextInputWidget
.prototype.select = function () {
10471 return this.selectRange( 0, this.getInputLength() );
10475 * Focus the input and move the cursor to the start.
10479 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10480 return this.selectRange( 0 );
10484 * Focus the input and move the cursor to the end.
10488 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10489 return this.selectRange( this.getInputLength() );
10493 * Insert new content into the input.
10495 * @param {string} content Content to be inserted
10498 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10500 range
= this.getRange(),
10501 value
= this.getValue();
10503 start
= Math
.min( range
.from, range
.to
);
10504 end
= Math
.max( range
.from, range
.to
);
10506 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10507 this.selectRange( start
+ content
.length
);
10512 * Insert new content either side of a selection.
10514 * @param {string} pre Content to be inserted before the selection
10515 * @param {string} post Content to be inserted after the selection
10518 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10520 range
= this.getRange(),
10521 offset
= pre
.length
;
10523 start
= Math
.min( range
.from, range
.to
);
10524 end
= Math
.max( range
.from, range
.to
);
10526 this.selectRange( start
).insertContent( pre
);
10527 this.selectRange( offset
+ end
).insertContent( post
);
10529 this.selectRange( offset
+ start
, offset
+ end
);
10534 * Set the validation pattern.
10536 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10537 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10538 * value must contain only numbers).
10540 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10541 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10543 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10544 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10545 this.validate
= validate
;
10547 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10552 * Sets the 'invalid' flag appropriately.
10554 * @param {boolean} [isValid] Optionally override validation result
10556 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10558 setFlag = function ( valid
) {
10560 widget
.$input
.attr( 'aria-invalid', 'true' );
10562 widget
.$input
.removeAttr( 'aria-invalid' );
10564 widget
.setFlags( { invalid
: !valid
} );
10567 if ( isValid
!== undefined ) {
10568 setFlag( isValid
);
10570 this.getValidity().then( function () {
10579 * Get the validity of current value.
10581 * This method returns a promise that resolves if the value is valid and rejects if
10582 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10584 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10586 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10589 function rejectOrResolve( valid
) {
10591 return $.Deferred().resolve().promise();
10593 return $.Deferred().reject().promise();
10597 // Check browser validity and reject if it is invalid
10599 this.$input
[ 0 ].checkValidity
!== undefined &&
10600 this.$input
[ 0 ].checkValidity() === false
10602 return rejectOrResolve( false );
10605 // Run our checks if the browser thinks the field is valid
10606 if ( this.validate
instanceof Function
) {
10607 result
= this.validate( this.getValue() );
10608 if ( result
&& $.isFunction( result
.promise
) ) {
10609 return result
.promise().then( function ( valid
) {
10610 return rejectOrResolve( valid
);
10613 return rejectOrResolve( result
);
10616 return rejectOrResolve( this.getValue().match( this.validate
) );
10621 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10623 * @param {string} labelPosition Label position, 'before' or 'after'
10626 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10627 this.labelPosition
= labelPosition
;
10628 if ( this.label
) {
10629 // If there is no label and we only change the position, #updatePosition is a no-op,
10630 // but it takes really a lot of work to do nothing.
10631 this.updatePosition();
10637 * Update the position of the inline label.
10639 * This method is called by #setLabelPosition, and can also be called on its own if
10640 * something causes the label to be mispositioned.
10644 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10645 var after
= this.labelPosition
=== 'after';
10648 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10649 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10651 this.valCache
= null;
10652 this.scrollWidth
= null;
10653 this.positionLabel();
10659 * Position the label by setting the correct padding on the input.
10664 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10665 var after
, rtl
, property
, newCss
;
10667 if ( this.isWaitingToBeAttached
) {
10668 // #onElementAttach will be called soon, which calls this method
10673 'padding-right': '',
10677 if ( this.label
) {
10678 this.$element
.append( this.$label
);
10680 this.$label
.detach();
10681 // Clear old values if present
10682 this.$input
.css( newCss
);
10686 after
= this.labelPosition
=== 'after';
10687 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10688 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10690 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10691 // We have to clear the padding on the other side, in case the element direction changed
10692 this.$input
.css( newCss
);
10699 * @extends OO.ui.TextInputWidget
10702 * @param {Object} [config] Configuration options
10704 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10705 config
= $.extend( {
10709 // Parent constructor
10710 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10713 this.connect( this, {
10718 this.updateSearchIndicator();
10719 this.connect( this, {
10720 disable
: 'onDisable'
10726 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10734 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
10741 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10742 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10743 // Clear the text field
10744 this.setValue( '' );
10751 * Update the 'clear' indicator displayed on type: 'search' text
10752 * fields, hiding it when the field is already empty or when it's not
10755 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10756 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10757 this.setIndicator( null );
10759 this.setIndicator( 'clear' );
10764 * Handle change events.
10768 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10769 this.updateSearchIndicator();
10773 * Handle disable events.
10775 * @param {boolean} disabled Element is disabled
10778 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10779 this.updateSearchIndicator();
10785 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10786 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10787 this.updateSearchIndicator();
10793 * @extends OO.ui.TextInputWidget
10796 * @param {Object} [config] Configuration options
10797 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10798 * specifies minimum number of rows to display.
10799 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10800 * Use the #maxRows config to specify a maximum number of displayed rows.
10801 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
10802 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10804 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
10805 config
= $.extend( {
10808 config
.multiline
= false;
10809 // Parent constructor
10810 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
10813 this.multiline
= true;
10814 this.autosize
= !!config
.autosize
;
10815 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
10816 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
10818 // Clone for resizing
10819 if ( this.autosize
) {
10820 this.$clone
= this.$input
10822 .insertAfter( this.$input
)
10823 .attr( 'aria-hidden', 'true' )
10824 .addClass( 'oo-ui-element-hidden' );
10828 this.connect( this, {
10833 if ( this.multiline
&& config
.rows
) {
10834 this.$input
.attr( 'rows', config
.rows
);
10836 if ( this.autosize
) {
10837 this.isWaitingToBeAttached
= true;
10838 this.installParentChangeDetector();
10844 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
10846 /* Static Methods */
10851 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10852 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10853 state
.scrollTop
= config
.$input
.scrollTop();
10862 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
10863 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
10868 * Handle change events.
10872 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
10879 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
10880 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
10887 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
10889 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
10891 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
10892 // Some platforms emit keycode 10 for ctrl+enter in a textarea
10895 this.emit( 'enter', e
);
10900 * Automatically adjust the size of the text input.
10902 * This only affects multiline inputs that are {@link #autosize autosized}.
10907 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
10908 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
10909 idealHeight
, newHeight
, scrollWidth
, property
;
10911 if ( this.$input
.val() !== this.valCache
) {
10912 if ( this.autosize
) {
10914 .val( this.$input
.val() )
10915 .attr( 'rows', this.minRows
)
10916 // Set inline height property to 0 to measure scroll height
10917 .css( 'height', 0 );
10919 this.$clone
.removeClass( 'oo-ui-element-hidden' );
10921 this.valCache
= this.$input
.val();
10923 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
10925 // Remove inline height property to measure natural heights
10926 this.$clone
.css( 'height', '' );
10927 innerHeight
= this.$clone
.innerHeight();
10928 outerHeight
= this.$clone
.outerHeight();
10930 // Measure max rows height
10932 .attr( 'rows', this.maxRows
)
10933 .css( 'height', 'auto' )
10935 maxInnerHeight
= this.$clone
.innerHeight();
10937 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10938 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10939 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
10940 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
10942 this.$clone
.addClass( 'oo-ui-element-hidden' );
10944 // Only apply inline height when expansion beyond natural height is needed
10945 // Use the difference between the inner and outer height as a buffer
10946 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
10947 if ( newHeight
!== this.styleHeight
) {
10948 this.$input
.css( 'height', newHeight
);
10949 this.styleHeight
= newHeight
;
10950 this.emit( 'resize' );
10953 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
10954 if ( scrollWidth
!== this.scrollWidth
) {
10955 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10957 this.$label
.css( { right
: '', left
: '' } );
10958 this.$indicator
.css( { right
: '', left
: '' } );
10960 if ( scrollWidth
) {
10961 this.$indicator
.css( property
, scrollWidth
);
10962 if ( this.labelPosition
=== 'after' ) {
10963 this.$label
.css( property
, scrollWidth
);
10967 this.scrollWidth
= scrollWidth
;
10968 this.positionLabel();
10978 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
10979 return $( '<textarea>' );
10983 * Check if the input supports multiple lines.
10985 * @return {boolean}
10987 OO
.ui
.MultilineTextInputWidget
.prototype.isMultiline = function () {
10988 return !!this.multiline
;
10992 * Check if the input automatically adjusts its size.
10994 * @return {boolean}
10996 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
10997 return !!this.autosize
;
11003 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11004 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11005 if ( state
.scrollTop
!== undefined ) {
11006 this.$input
.scrollTop( state
.scrollTop
);
11011 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11012 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11013 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11015 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11016 * option, that option will appear to be selected.
11017 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11020 * After the user chooses an option, its `data` will be used as a new value for the widget.
11021 * A `label` also can be specified for each option: if given, it will be shown instead of the
11022 * `data` in the dropdown menu.
11024 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11026 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11029 * // Example: A ComboBoxInputWidget.
11030 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11031 * value: 'Option 1',
11033 * { data: 'Option 1' },
11034 * { data: 'Option 2' },
11035 * { data: 'Option 3' }
11038 * $( 'body' ).append( comboBox.$element );
11041 * // Example: A ComboBoxInputWidget with additional option labels.
11042 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11043 * value: 'Option 1',
11046 * data: 'Option 1',
11047 * label: 'Option One'
11050 * data: 'Option 2',
11051 * label: 'Option Two'
11054 * data: 'Option 3',
11055 * label: 'Option Three'
11059 * $( 'body' ).append( comboBox.$element );
11061 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11064 * @extends OO.ui.TextInputWidget
11067 * @param {Object} [config] Configuration options
11068 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11069 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11070 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11071 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11072 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11073 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11075 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11076 // Configuration initialization
11077 config
= $.extend( {
11078 autocomplete
: false
11081 // ComboBoxInputWidget shouldn't support `multiline`
11082 config
.multiline
= false;
11084 // See InputWidget#reusePreInfuseDOM about `config.$input`
11085 if ( config
.$input
) {
11086 config
.$input
.removeAttr( 'list' );
11089 // Parent constructor
11090 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11093 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11094 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11095 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11097 disabled
: this.disabled
11099 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11103 $floatableContainer
: this.$element
,
11104 disabled
: this.isDisabled()
11110 this.connect( this, {
11111 change
: 'onInputChange',
11112 enter
: 'onInputEnter'
11114 this.dropdownButton
.connect( this, {
11115 click
: 'onDropdownButtonClick'
11117 this.menu
.connect( this, {
11118 choose
: 'onMenuChoose',
11119 add
: 'onMenuItemsChange',
11120 remove
: 'onMenuItemsChange',
11121 toggle
: 'onMenuToggle'
11125 this.$input
.attr( {
11127 'aria-owns': this.menu
.getElementId(),
11128 'aria-autocomplete': 'list'
11130 // Do not override options set via config.menu.items
11131 if ( config
.options
!== undefined ) {
11132 this.setOptions( config
.options
);
11134 this.$field
= $( '<div>' )
11135 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11136 .append( this.$input
, this.dropdownButton
.$element
);
11138 .addClass( 'oo-ui-comboBoxInputWidget' )
11139 .append( this.$field
);
11140 this.$overlay
.append( this.menu
.$element
);
11141 this.onMenuItemsChange();
11146 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11151 * Get the combobox's menu.
11153 * @return {OO.ui.MenuSelectWidget} Menu widget
11155 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11160 * Get the combobox's text input widget.
11162 * @return {OO.ui.TextInputWidget} Text input widget
11164 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11169 * Handle input change events.
11172 * @param {string} value New value
11174 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11175 var match
= this.menu
.findItemFromData( value
);
11177 this.menu
.selectItem( match
);
11178 if ( this.menu
.findHighlightedItem() ) {
11179 this.menu
.highlightItem( match
);
11182 if ( !this.isDisabled() ) {
11183 this.menu
.toggle( true );
11188 * Handle input enter events.
11192 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11193 if ( !this.isDisabled() ) {
11194 this.menu
.toggle( false );
11199 * Handle button click events.
11203 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11204 this.menu
.toggle();
11209 * Handle menu choose events.
11212 * @param {OO.ui.OptionWidget} item Chosen item
11214 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11215 this.setValue( item
.getData() );
11219 * Handle menu item change events.
11223 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11224 var match
= this.menu
.findItemFromData( this.getValue() );
11225 this.menu
.selectItem( match
);
11226 if ( this.menu
.findHighlightedItem() ) {
11227 this.menu
.highlightItem( match
);
11229 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11233 * Handle menu toggle events.
11236 * @param {boolean} isVisible Open state of the menu
11238 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11239 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11245 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11247 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11249 if ( this.dropdownButton
) {
11250 this.dropdownButton
.setDisabled( this.isDisabled() );
11253 this.menu
.setDisabled( this.isDisabled() );
11260 * Set the options available for this input.
11262 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11265 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11268 .addItems( options
.map( function ( opt
) {
11269 return new OO
.ui
.MenuOptionWidget( {
11271 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11279 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11280 * which is a widget that is specified by reference before any optional configuration settings.
11282 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11284 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11285 * A left-alignment is used for forms with many fields.
11286 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11287 * A right-alignment is used for long but familiar forms which users tab through,
11288 * verifying the current field with a quick glance at the label.
11289 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11290 * that users fill out from top to bottom.
11291 * - **inline**: The label is placed after the field-widget and aligned to the left.
11292 * An inline-alignment is best used with checkboxes or radio buttons.
11294 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
11295 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11297 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11300 * @extends OO.ui.Layout
11301 * @mixins OO.ui.mixin.LabelElement
11302 * @mixins OO.ui.mixin.TitledElement
11305 * @param {OO.ui.Widget} fieldWidget Field widget
11306 * @param {Object} [config] Configuration options
11307 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
11308 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
11309 * The array may contain strings or OO.ui.HtmlSnippet instances.
11310 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
11311 * The array may contain strings or OO.ui.HtmlSnippet instances.
11312 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11313 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11314 * For important messages, you are advised to use `notices`, as they are always shown.
11315 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11316 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11318 * @throws {Error} An error is thrown if no widget is specified
11320 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11321 // Allow passing positional parameters inside the config object
11322 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11323 config
= fieldWidget
;
11324 fieldWidget
= config
.fieldWidget
;
11327 // Make sure we have required constructor arguments
11328 if ( fieldWidget
=== undefined ) {
11329 throw new Error( 'Widget not found' );
11332 // Configuration initialization
11333 config
= $.extend( { align
: 'left' }, config
);
11335 // Parent constructor
11336 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11338 // Mixin constructors
11339 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11340 $label
: $( '<label>' )
11342 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11345 this.fieldWidget
= fieldWidget
;
11348 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11349 this.$messages
= $( '<ul>' );
11350 this.$header
= $( '<span>' );
11351 this.$body
= $( '<div>' );
11353 if ( config
.help
) {
11354 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11355 $overlay
: config
.$overlay
,
11359 classes
: [ 'oo-ui-fieldLayout-help' ],
11363 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11364 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11366 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11368 this.$help
= this.popupButtonWidget
.$element
;
11370 this.$help
= $( [] );
11374 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11377 if ( config
.help
) {
11378 // Set the 'aria-describedby' attribute on the fieldWidget
11379 // Preference given to an input or a button
11381 this.fieldWidget
.$input
||
11382 this.fieldWidget
.$button
||
11383 this.fieldWidget
.$element
11385 'aria-describedby',
11386 this.popupButtonWidget
.getPopup().getBodyId()
11389 if ( this.fieldWidget
.getInputId() ) {
11390 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11392 this.$label
.on( 'click', function () {
11393 this.fieldWidget
.simulateLabelClick();
11397 .addClass( 'oo-ui-fieldLayout' )
11398 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11399 .append( this.$body
);
11400 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11401 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11402 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11404 .addClass( 'oo-ui-fieldLayout-field' )
11405 .append( this.fieldWidget
.$element
);
11407 this.setErrors( config
.errors
|| [] );
11408 this.setNotices( config
.notices
|| [] );
11409 this.setAlignment( config
.align
);
11410 // Call this again to take into account the widget's accessKey
11411 this.updateTitle();
11416 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11417 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11418 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11423 * Handle field disable events.
11426 * @param {boolean} value Field is disabled
11428 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11429 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11433 * Get the widget contained by the field.
11435 * @return {OO.ui.Widget} Field widget
11437 OO
.ui
.FieldLayout
.prototype.getField = function () {
11438 return this.fieldWidget
;
11442 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11443 * #setAlignment). Return `false` if it can't or if this can't be determined.
11445 * @return {boolean}
11447 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11448 // This is very simplistic, but should be good enough.
11449 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11454 * @param {string} kind 'error' or 'notice'
11455 * @param {string|OO.ui.HtmlSnippet} text
11458 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11459 var $listItem
, $icon
, message
;
11460 $listItem
= $( '<li>' );
11461 if ( kind
=== 'error' ) {
11462 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11463 $listItem
.attr( 'role', 'alert' );
11464 } else if ( kind
=== 'notice' ) {
11465 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
11469 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11471 .append( $icon
, message
.$element
)
11472 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11477 * Set the field alignment mode.
11480 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11483 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11484 if ( value
!== this.align
) {
11485 // Default to 'left'
11486 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11490 if ( value
=== 'inline' && !this.isFieldInline() ) {
11493 // Reorder elements
11494 if ( value
=== 'top' ) {
11495 this.$header
.append( this.$help
, this.$label
);
11496 this.$body
.append( this.$header
, this.$field
);
11497 } else if ( value
=== 'inline' ) {
11498 this.$header
.append( this.$help
, this.$label
);
11499 this.$body
.append( this.$field
, this.$header
);
11501 this.$header
.append( this.$label
);
11502 this.$body
.append( this.$header
, this.$help
, this.$field
);
11504 // Set classes. The following classes can be used here:
11505 // * oo-ui-fieldLayout-align-left
11506 // * oo-ui-fieldLayout-align-right
11507 // * oo-ui-fieldLayout-align-top
11508 // * oo-ui-fieldLayout-align-inline
11509 if ( this.align
) {
11510 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11512 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11513 this.align
= value
;
11520 * Set the list of error messages.
11522 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11523 * The array may contain strings or OO.ui.HtmlSnippet instances.
11526 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11527 this.errors
= errors
.slice();
11528 this.updateMessages();
11533 * Set the list of notice messages.
11535 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11536 * The array may contain strings or OO.ui.HtmlSnippet instances.
11539 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11540 this.notices
= notices
.slice();
11541 this.updateMessages();
11546 * Update the rendering of error and notice messages.
11550 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11552 this.$messages
.empty();
11554 if ( this.errors
.length
|| this.notices
.length
) {
11555 this.$body
.after( this.$messages
);
11557 this.$messages
.remove();
11561 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11562 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11564 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11565 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11570 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11571 * (This is a bit of a hack.)
11574 * @param {string} title Tooltip label for 'title' attribute
11577 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11578 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11579 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11585 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11586 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11587 * is required and is specified before any optional configuration settings.
11589 * Labels can be aligned in one of four ways:
11591 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11592 * A left-alignment is used for forms with many fields.
11593 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11594 * A right-alignment is used for long but familiar forms which users tab through,
11595 * verifying the current field with a quick glance at the label.
11596 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11597 * that users fill out from top to bottom.
11598 * - **inline**: The label is placed after the field-widget and aligned to the left.
11599 * An inline-alignment is best used with checkboxes or radio buttons.
11601 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11602 * text is specified.
11605 * // Example of an ActionFieldLayout
11606 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11607 * new OO.ui.TextInputWidget( {
11608 * placeholder: 'Field widget'
11610 * new OO.ui.ButtonWidget( {
11614 * label: 'An ActionFieldLayout. This label is aligned top',
11616 * help: 'This is help text'
11620 * $( 'body' ).append( actionFieldLayout.$element );
11623 * @extends OO.ui.FieldLayout
11626 * @param {OO.ui.Widget} fieldWidget Field widget
11627 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11628 * @param {Object} config
11630 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11631 // Allow passing positional parameters inside the config object
11632 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11633 config
= fieldWidget
;
11634 fieldWidget
= config
.fieldWidget
;
11635 buttonWidget
= config
.buttonWidget
;
11638 // Parent constructor
11639 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11642 this.buttonWidget
= buttonWidget
;
11643 this.$button
= $( '<span>' );
11644 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11648 .addClass( 'oo-ui-actionFieldLayout' );
11650 .addClass( 'oo-ui-actionFieldLayout-button' )
11651 .append( this.buttonWidget
.$element
);
11653 .addClass( 'oo-ui-actionFieldLayout-input' )
11654 .append( this.fieldWidget
.$element
);
11656 .append( this.$input
, this.$button
);
11661 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
11664 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11665 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11666 * configured with a label as well. For more information and examples,
11667 * please see the [OOUI documentation on MediaWiki][1].
11670 * // Example of a fieldset layout
11671 * var input1 = new OO.ui.TextInputWidget( {
11672 * placeholder: 'A text input field'
11675 * var input2 = new OO.ui.TextInputWidget( {
11676 * placeholder: 'A text input field'
11679 * var fieldset = new OO.ui.FieldsetLayout( {
11680 * label: 'Example of a fieldset layout'
11683 * fieldset.addItems( [
11684 * new OO.ui.FieldLayout( input1, {
11685 * label: 'Field One'
11687 * new OO.ui.FieldLayout( input2, {
11688 * label: 'Field Two'
11691 * $( 'body' ).append( fieldset.$element );
11693 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11696 * @extends OO.ui.Layout
11697 * @mixins OO.ui.mixin.IconElement
11698 * @mixins OO.ui.mixin.LabelElement
11699 * @mixins OO.ui.mixin.GroupElement
11702 * @param {Object} [config] Configuration options
11703 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11704 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11705 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11706 * For important messages, you are advised to use `notices`, as they are always shown.
11707 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11708 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11710 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
11711 // Configuration initialization
11712 config
= config
|| {};
11714 // Parent constructor
11715 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
11717 // Mixin constructors
11718 OO
.ui
.mixin
.IconElement
.call( this, config
);
11719 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11720 OO
.ui
.mixin
.GroupElement
.call( this, config
);
11723 this.$header
= $( '<legend>' );
11724 if ( config
.help
) {
11725 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11726 $overlay
: config
.$overlay
,
11730 classes
: [ 'oo-ui-fieldsetLayout-help' ],
11734 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11735 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11737 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11739 this.$help
= this.popupButtonWidget
.$element
;
11741 this.$help
= $( [] );
11746 .addClass( 'oo-ui-fieldsetLayout-header' )
11747 .append( this.$icon
, this.$label
, this.$help
);
11748 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
11750 .addClass( 'oo-ui-fieldsetLayout' )
11751 .prepend( this.$header
, this.$group
);
11752 if ( Array
.isArray( config
.items
) ) {
11753 this.addItems( config
.items
);
11759 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
11760 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
11761 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
11762 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
11764 /* Static Properties */
11770 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
11773 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11774 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11775 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11776 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
11778 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11779 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11780 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11781 * some fancier controls. Some controls have both regular and InputWidget variants, for example
11782 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11783 * often have simplified APIs to match the capabilities of HTML forms.
11784 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
11786 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
11787 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
11790 * // Example of a form layout that wraps a fieldset layout
11791 * var input1 = new OO.ui.TextInputWidget( {
11792 * placeholder: 'Username'
11794 * var input2 = new OO.ui.TextInputWidget( {
11795 * placeholder: 'Password',
11798 * var submit = new OO.ui.ButtonInputWidget( {
11802 * var fieldset = new OO.ui.FieldsetLayout( {
11803 * label: 'A form layout'
11805 * fieldset.addItems( [
11806 * new OO.ui.FieldLayout( input1, {
11807 * label: 'Username',
11810 * new OO.ui.FieldLayout( input2, {
11811 * label: 'Password',
11814 * new OO.ui.FieldLayout( submit )
11816 * var form = new OO.ui.FormLayout( {
11817 * items: [ fieldset ],
11818 * action: '/api/formhandler',
11821 * $( 'body' ).append( form.$element );
11824 * @extends OO.ui.Layout
11825 * @mixins OO.ui.mixin.GroupElement
11828 * @param {Object} [config] Configuration options
11829 * @cfg {string} [method] HTML form `method` attribute
11830 * @cfg {string} [action] HTML form `action` attribute
11831 * @cfg {string} [enctype] HTML form `enctype` attribute
11832 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11834 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
11837 // Configuration initialization
11838 config
= config
|| {};
11840 // Parent constructor
11841 OO
.ui
.FormLayout
.parent
.call( this, config
);
11843 // Mixin constructors
11844 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11847 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11849 // Make sure the action is safe
11850 action
= config
.action
;
11851 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11852 action
= './' + action
;
11857 .addClass( 'oo-ui-formLayout' )
11859 method
: config
.method
,
11861 enctype
: config
.enctype
11863 if ( Array
.isArray( config
.items
) ) {
11864 this.addItems( config
.items
);
11870 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11871 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11876 * A 'submit' event is emitted when the form is submitted.
11881 /* Static Properties */
11887 OO
.ui
.FormLayout
.static.tagName
= 'form';
11892 * Handle form submit events.
11895 * @param {jQuery.Event} e Submit event
11898 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11899 if ( this.emit( 'submit' ) ) {
11905 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11906 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11909 * // Example of a panel layout
11910 * var panel = new OO.ui.PanelLayout( {
11914 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
11916 * $( 'body' ).append( panel.$element );
11919 * @extends OO.ui.Layout
11922 * @param {Object} [config] Configuration options
11923 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11924 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11925 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11926 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11928 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
11929 // Configuration initialization
11930 config
= $.extend( {
11937 // Parent constructor
11938 OO
.ui
.PanelLayout
.parent
.call( this, config
);
11941 this.$element
.addClass( 'oo-ui-panelLayout' );
11942 if ( config
.scrollable
) {
11943 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
11945 if ( config
.padded
) {
11946 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
11948 if ( config
.expanded
) {
11949 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
11951 if ( config
.framed
) {
11952 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
11958 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
11963 * Focus the panel layout
11965 * The default implementation just focuses the first focusable element in the panel
11967 OO
.ui
.PanelLayout
.prototype.focus = function () {
11968 OO
.ui
.findFocusable( this.$element
).focus();
11972 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11973 * items), with small margins between them. Convenient when you need to put a number of block-level
11974 * widgets on a single line next to each other.
11976 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11979 * // HorizontalLayout with a text input and a label
11980 * var layout = new OO.ui.HorizontalLayout( {
11982 * new OO.ui.LabelWidget( { label: 'Label' } ),
11983 * new OO.ui.TextInputWidget( { value: 'Text' } )
11986 * $( 'body' ).append( layout.$element );
11989 * @extends OO.ui.Layout
11990 * @mixins OO.ui.mixin.GroupElement
11993 * @param {Object} [config] Configuration options
11994 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11996 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
11997 // Configuration initialization
11998 config
= config
|| {};
12000 // Parent constructor
12001 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12003 // Mixin constructors
12004 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12007 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12008 if ( Array
.isArray( config
.items
) ) {
12009 this.addItems( config
.items
);
12015 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12016 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12020 //# sourceMappingURL=oojs-ui-core.js.map